Skip to content

Latest commit

 

History

History

part-14

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Part 14 - classes 3: inheritance, super

In this part of the series, we implement class inheritance, and the super expression. As part of these changes, we also introduce Object, which is a common superclass of all objects in JavaScript, and implement one of its several instance methods in JavaScript, hasOwnProperty().

Grammar

In order to support these features, we need to introduce two changes to the ANTLR grammar for EasyScript:

  1. We change the class declaration statement to add the optional extends clause.
  2. We add a new expr6 production that represents the super keyword.

Parsing

Our parser needs a few changes. It now takes an instance of the ObjectPrototype class as an argument, which represents the prototype of the built-in Object class (the only class without a parent class in the language). We save it in the Stack of Maps that we use for tracking function arguments and local variables under the key "Object", since user-defined classes can now extend it explicitly (and you can also create instances of it, in code like new Object()).

When parsing a class declaration statement, we handle the extends clause by searching in the first Map in the Stack for the prototype with that name, and then save the prototype of the currently parsed class in that same Map, so subsequent class declarations can reference it.

We also save the prototype of the class being currently parsed in a field of the parser, so that we can pass it when encountering a super keyword (super, unlike this, which is dynamic, is static in virtually all object-oriented languages, which means it always refers to a specific class, regardless of the runtime type of a given instance).

Unifying objects and prototypes

Since with inheritance, prototypes have themselves parent prototypes, we unify the JavaScriptObject and ClassPrototypeObject classes by making ClassPrototypeObject extend JavaScriptObject to avoid duplicating code between the two.

We change JavaScriptObject to use the interop library instead of the dynamic object library when reading properties of its prototype, which allows classes to inherit methods from their superclass.

Because of that change, we have to modify the type of the prototype in JavaScriptObject from ClassPrototypeObject to Truffle's DynamicObject, as keeping it as ClassPrototypeObject, which now extends JavaScriptObject, would make ClassPrototypeObject impossible to instantiate, as it would always require another instance of ClassPrototypeObject to be provided in its constructor.

To start the chain of ClassPrototypeObjects, we need to have a prototype without a parent prototype (the aforementioned ObjectPrototype class), which extends ClassPrototypeObject by providing an anonymous subclass of DynamicObject as the prototype (we can't pass null there, as JavaScriptObject object uses @CachedLibrary with the prototype field, and you cannot use @CachedLibrary with a null value), and then overrides the implementations of the property read messages from the interop library inherited from JavaScriptObject to not reference the prototype field.

We create an instance of ObjectPrototype in the TruffleLanguage class for this part, save it as a field next to the Shapes, and pass it to the parser in the parse() method. We also save it inside the ShapesAndPrototypes class that we pass to the TruffleLanguage context class for this part.

Constructor inheritance

Since constructors are regular properties in JavaScript, they are also inherited from superclasses. Because of that, we need to change the NewExprNode class to use the interop library instead of dynamic object library, since the constructor of a given class might be inherited from an ancestor class.

Object methods

The implementation of the Object.hasOwnProperty() method is in the HasOwnPropertyMethodBodyExprNode class, and is very similar to the other built-in functions and methods, like String.charAt().

In order to find this method when invoked on strings and primitives, we need to modify ReadTruffleStringPropertyNode and CommonReadPropertyNode to read from the string or object prototype, respectively, in their last specializations.

In ReadTruffleStringPropertyNode, we need to convert any property we receive to a string, we introduce a new method to the EasyScriptTruffleStrings class, toStringOfMaybeString(), that is deliberately not annotated with the @TruffleBoundary annotation that first checks whether the argument it's given is already a Java String, in code like "a".charAt(), before delegating to toString() from the previous part, which improves performance.

super() in constructors

In order to allow calling parent constructors with super(), we need to implement the evaluateAsReceiver() and evaluateAsFunction() methods from the previous part in the SuperExprNode class. evaluateAsReceiver() is the same as this, while evaluateAsFunction() needs to find the "constructor" property in the prototype of the parent class. Since we need to use the interop library to find that property, as the constructor might have been defined on an ancestor class of the parent class, we have to use the Node.insert() method to save an instance of it in a field, similarly to what @CachedLibrary does (we can't use @CachedLibrary directly, since evaluateAsFunction() is not a specialization method).

super property reads

For reading properties of super, since we need to change the algorithm of finding the property to start with the parent class prototype, instead of this object. We can implement that by treating SuperExprNode specially in PropertyReadExprNode and ArrayIndexReadExprNode to read the parent prototype from the SuperExprNode instance with the readParentPrototype() method.

Note: we don't have to do the same with the expression Nodes for writing properties, PropertyWriteExprNode and ArrayIndexWriteExprNode, since writing to super writes to this in JavaScript.

Benchmark

We modify the benchmark from the last part to add a class hierarchy to the Counter class.

Here are the results when running the benchmark on my laptop:

Benchmark                                                Mode  Cnt    Score    Error  Units
CounterThisBenchmark.count_with_this_in_for_direct_ezs   avgt    5  582.213 ± 19.996  us/op
CounterThisBenchmark.count_with_this_in_for_direct_js    avgt    5  705.399 ± 16.581  us/op
CounterThisBenchmark.count_with_this_in_for_indexed_ezs  avgt    5  575.528 ± 14.741  us/op
CounterThisBenchmark.count_with_this_in_for_indexed_js   avgt    5  707.888 ± 18.730  us/op

The EasyScript performance is identical to the last part, while GraalVM JavaScript is slightly slower -- I assume because it's possible to change the prototype of an object in JavaScript with the Object.setPrototype() method, while the prototype of an object cannot be changed after instantiating it in EasyScript, which might allow Graal to apply more aggressive optimizations in that case.


In addition to the benchmark, there are some unit tests that validate the inheritance functionality works as expected.