In this part of the series, we add support for (global) variables to the language.
The grammar is in the EasyScript.g4
file.
We add statements to our language --
parsing our program will now result in a list of statements --
and also expression precedence,
so that a = 1 + 1
is parsed as a = (1 + 1)
,
and not (a = 1) + 1
.
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.
We handle a single declaration creating multiple new variables in code like
let a, b;
by transforming it into the equivalent let a; let b;
.
If a variable declaration does not have an initializer,
we create it with the undefined
literal expression as the initializer,
basically transforming let c;
into the equivalent let c = undefined;
.
We also use a LanguageContext
class in this version of our TruffleLanguage
.
Our EasyScriptLanguageContext
class
contains the GlobalScopeObject
instance which stores the values of the global variables.
We return that object from the
TruffleLanguage.getScope()
method,
which allows retrieving the global variables using the
getBindings(String languageId)
method of the GraalVM polyglot Context
class.
The GlobalScopeObject
class
stores our global variables in a regular Java Map
.
It also saves which variables are const
,
as any attempt to update them should result in an error.
It has methods that can be used to create, update and retrieve variables,
which will be invoked from the new Truffle AST Nodes.
Since this object is returned from the
getBindings(String languageId)
method of the GraalVM polyglot Context
class,
it needs to be a GraalVM interop object.
This means implementing the
TruffleObject
interface,
and also messages from
Truffle's interoperability library.
You implement this library by annotating your class with the
@ExportLibrary
annotation,
passing it the InteropLibrary
class,
and then write instance methods for each message you want to implement --
in the case of GlobalScopeObject
,
we implement messages that allow reading the values of the global variables.
Each message method needs to be annotated with the
@ExportMessage
annotation,
and its name must either match the name of the message,
or you must use the name
attribute of the @ExportMessage
,
and pass the name of the message there.
Note that the first argument of the messages in
Truffle's interoperability library
is the receiver,
which is assumed to be your class when implementing the messages,
so your instance methods should skip the first argument when implementing the messages.
Also note that the message methods do not have to be public
--
it's common to make them package-private,
in order to not pollute the public API of the class with these interop-specific methods.
Since we can now return an instance of the
Undefined
class,
which represents the JavaScript undefined
vale,
when evaluating EasyScript code
(in programs like let a; a
),
it also needs to be a GraalVM interop object,
same as GlobalScopeObject
.
In this case, we only implement the
isNull()
and
toDisplayString(boolean)
messages.
We separate the AST Nodes into two hierarchies: statements, and expressions.
For statements, we add a Node representing the
declaration of a new variable.
It uses the Truffle DSL that we learned about in
part 3.
The @NodeField
annotations allow us to add fields to the subclass generated by the DSL
(which we can access in the superclass by declaring abstract getters for them).
We get a reference to the Language Context,
that contains our global scope object,
using the currentLanguageContext()
method,
which we inherit from the
common ancestor of all Nodes,
and which gets it from the
TruffleLanguage.ContextReference
static field in EasyScriptLanguageContext
.
In addition to GlobalVarDeclStmtNode
,
we also add an
expression statement Node,
from which we simply return the value of the evaluated expression.
For expressions, we add the
assignment expression Node,
which looks almost identical to the declaration statement,
just updating the variable instead of creating it.
We also have a new
reference expression Node,
which reads the value of a (global) variable from GlobalScopeObject
,
and the
undefined
literal Node,
which simply returns Undefined.INSTANCE
from executeGeneric()
,
and throws UnexpectedResultException
from the remaining execute*()
methods.
The presence of undefined
also forces us to make a change to the
AdditionExprNode
,
to add a specialization handling it;
in accordance with JavaScript semantics,
we return Double.NaN
if any side of addition is undefined
.
Finally, we have our RootNode
.
It's very simple: it takes in a list of statements,
and in its execute()
method evaluates them all,
and returns the value of the last one.
Since it has a variable amount of children,
we need to use the @Children
annotation
instead of @Child
,
and Truffle actually requires using a Java array as the type of the field,
instead of a collection like List
or Set
.
Since arrays are itself mutable, you can also mark the entire field as final
,
which you can't do for @Child
fields.
There is a unit test exercising the positive test cases, and the possible errors, like duplicate variable declarations, or referencing an undeclared variable.