Skip to content

Latest commit

 

History

History

part-05

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Part 5 - global variables

In this part of the series, we add support for (global) variables to the language.

Grammar

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.

TruffleLanguage class

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.

GlobalScopeObject 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.

Undefined class

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.

Truffle AST Nodes

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.