In this part of the series, we add support for function calls to our language.
We implement two built-in JavaScript functions from the
Math
object,
Math.abs()
and
Math.pow()
.
The grammar is in the EasyScript.g4
file.
The only changes compared to the grammar from part 5
are a few new expression types: we add call expressions, of course,
but also complex references, so we can write Math.abs
.
We also introduce negation ("unary minus") expressions, like -3
,
so that we can test the Math.abs
function with negative numbers.
Our TruffleLanguage
class
implements the parse(ParsingRequest)
method identically like in the
previous part --
by delegating to the parser,
which uses the classes generated by ANTLR from the aforementioned
EasyScript.g4
file
to perform the parsing, and then translates the parse tree into the Truffle AST.
Note that we convert a complex reference like Math.abs
into a simple name "Math.abs"
.
Since we don't allow periods in the name of variables,
we know user-defined variables will never collide with these interpreter-created variables.
Inside the createContext()
method,
we populate the global scope
(which is the same global scope we introduced in the previous article)
with the built-in functions.
Each function is represented by the FunctionObject
class,
which contains a CallTarget
(something that we've been seeing since part 1),
which contains a RootNode
,
which contains the expression Node representing the body of the function.
This body Node has as its children as many
ReadFunctionArgExprNode
instances
as arguments that function takes.
This allows writing @Specialization
s in the expression body Node that simply use the values of the function arguments.
Note that, for the pow()
function,
we make its body expression Node,
the PowFunctionBodyExprNode
class,
inherit from the BuiltInFunctionBodyExprNode
class
which uses Truffle's @GenerateNodeFactory
annotation.
This makes the Truffle DSL generate a PowFunctionBodyExprNodeFactory
class which implements the NodeFactory<PowFunctionBodyExprNode>
interface.
That interface can then be used to write a helper method inside EasyScriptTruffleLanguage
(in our case, that method is called defineBuiltInFunction()
)
that reduces duplication when creating an instance of the built-in function's body expression Node.
The invoking side is implemented by the
FunctionCallExprNode
class.
It evaluates the target of the call, its arguments
(we have to evaluate them explicitly, in a loop,
as the Truffle DSL has a limitation where it doesn't support specializations with a variable amount of children Nodes),
and then delegates to a FunctionDispatchNode
.
The FunctionDispatchNode
class
uses specializations to optimize the performance of function calls,
depending on how stable the target of the call is.
Finally, we need to make our
FunctionObject
class
a polyglot value, similarly like we did for Undefined
in the
previous article,
as functions can now be returned from EasyScript code
(for example, in expressions like Math.abs;
).
So, we make FunctionObject
implement the TruffleObject
marker interface,
and, to allow our functions to be called from other languages,
override the isExecutable()
and execute()
messages
from the interop library.
When implementing execute()
,
we re-use the FunctionDispatchNode
we saw in FunctionCallExprNode
above,
so that polyglot calls are optimized the same as in-language calls.
However, note that we now need to check whether the values we're called with are actually valid EasyScript values,
as other languages have the ability to pass arbitrary types into our Nodes,
which only handle int
, double
, Undefined
and FunctionObject
types.
So, we perform a check inside FunctionObject.execute()
to make sure the passed arguments are of one of the types we allow.
There is a unit test exercising a few common scenarios with function calls.