GraalVM supports ECMAScript 2021 — and beyond

Christian Wirth
graalvm
Published in
11 min readMay 17, 2021

GraalVM is a modern, high performance JDK distribution — but its impact goes far beyond traditional Java applications.

GraalVM improves the performance of applications written not only in Java, but also in other JVM languages like Scala and Kotlin, etc. Its Native Image generator compiles Java applications ahead-of-time, transforming them into native executables with excellent startup performance and lower memory requirements. GraalVM also supports many other programming languages like JavaScript, Python, Ruby, Webassembly, R, and so on.

JavaScript landing page at www.graalvm.org

The JavaScript implementation, often called Graal.js, is actually GraalVM’s most complete implementation to this point. It supports the execution of standalone JavaScript applications, as well as JavaScript from within a Java application. It also comes with full support for Node.js and, notably, is compatible with the latest version of JavaScript’s language specification: the ECMAScript Language Specification, version 2021.

In this article we look at the features added to JavaScript by ECMAScript 2021, and share examples of how you can benefit from them by using GraalVM. Also, if you prefer watching, here’s a video on the same topic:

In this video Christian Wirth explains new features in ECMAScript 2021 and shows example of them using GraalVM

ECMAScript 2021

JavaScript is specified in the ECMAScript language specification. It is developed in an open-source process by the Technical Committee 39 (TC39) of ECMA, where many vendors, users, and contributors to JavaScript are members. Together, they make sure that JavaScript evolves, and evolves in a positive, maintainable, productive fashion. Maintaining “web compatibility” — not breaking existing code out there — is a major concern to the committee.

Oracle Labs is a part of TC39, having joined last year, and is actively participating. Being a member of the committee gives GraalVM first-hand insight into the future development of JavaScript and makes it possible to actively contribute interesting proposals to the specification process.

In March, 2021, a draft that will seemingly become ECMAScript 2021 was agreed upon by the TC39. It has yet to be approved by the General Assembly of ECMA, but that feels like a formality. All the interested parties already made sure that this draft does not break any existing code, allowing the General Assembly to approve a well formulated, well defined Specification.

Several features will officially be added to the language in ECMAScript 2021. In most implementations, those features are already available, either by default or behind flags. In GraalVM, those new ECMAScript 2021 features have been available by default, since GraalVM release 21.0.0. In older releases, you can get a subset of the features by explicitly setting the `js.ecmascript-version=2021` flag.

String.prototype.replaceAll

Let’s start with something simple: this new String method replaces all occurrences of a search pattern with a replace value.

> "the quick brown fox".replaceAll("o","_")
the quick br_wn f_x

Similar to the existing String.prototype.replace method, the search value can be a string or a regular expression, and the replace value can be a constant string or a replacer function that takes the found pattern as input. Previously, you needed to repeatedly call replace when you used it with a String pattern or you could also use a global regex pattern.

Graal.js supports this feature, no strings attached.

Private Methods

Private methods extend the existing possibilities you have with classes. Methods can now be declared private by prepending their name with a hash symbol:

class Example {
fnPublic() {
console.log("fnPublic called");
}
#fnPrivate() {
console.log("fnPrivate called");
}
}
const ex = new Example();
ex.fnPublic();
ex.#fnPrivate(); //fails with an error!

Note that you actually need the # symbol when calling the method from within the class. You can call such a method from within the class (e.g., you could call it from fnPublic()), but not from outside the class. This is similar to using the private keyword on a Java method, and similar features in other languages.

Private Accessors

Private class accessor methods are very similar to private methods mentioned above.

class Example {
get name() {
return "ECMAScript";
}
get #version() {
return 2021;
}
}
const ex = new Example();
console.log(ex.name);
console.log(ex.#version); //error!

By prepending the getter’s name with the # symbol, you make it private. It can be called from within the class, not from outside. This simplifies capsuling the data in your objects and can help you write better APIs.

Promise.any and AggregateError

This promise combinator aggregates several input promises, and resolves if any of those resolves. You can provide them as an iterable of promises and get one promise returned. This single promise resolves with the value of one of the resolved promises. If no input promise resolves, the combined promise is rejected with a new kind of error, an AggregateError.

const pr1 = new Promise(resolve => { resolve("pr1 worked"); });
const pr2 = new Promise(resolve => { resolve("pr2 worked"); });
//const pr2 = Promise.reject("pr2 failed");

Promise.any([pr1, pr2])
.then(result => console.log("result: "+result))
.catch(err => console.log("error: "+err+" "+err.errors));

Promise.any is similar to Promise.all, but will resolve if at least one of the inputs resolves.

In case all input promises are rejected, you will receive an AggregateError in your catch clause. This combines all the rejection messages of the individual promises. Those messages can be queried with the errors property:

Promise.any([pr1, pr2])
.then(result => console.log("result: "+result))
.catch(err => console.log("error: "+err+" "+err.errors));

The AggregateError thus can help you understand why the promises failed, allowing you to react accordingly.

WeakRef and FinalizationRegistry

Weak references allow to store references to objects that might need to be garbage collected. JavaScript has an automated memory management system. It makes sure that all objects (and arrays, functions, etc.,) that you can still reach from your application are kept alive — but those that are not reachable any more, because their scope is already closed, are removed from memory.

Sometimes this can lead to situations where you store an object that is not needed any longer — and you cause what is called a memory leak. A typical example is a cache that has the object as key. This situation is prone to retaining a strong reference on the object in the cache, while the object might not actually be used any longer (outside the cache). The cache causes a memory leak: it “keeps objects alive” that would otherwise be long gone. Without any strategy to ever remove objects again from the cache, the memory consumption of your application will continue to rise until the process is out of memory.

A weak reference can solve that problem. It holds a reference to an object, but still allows the memory management system to remove the object if there is no strong reference to it any longer. So, as soon as all strong references are gone and only one or more weak references to the object exist, the garbage collector can remove the object. You would receive a `null` value then from your weak reference after that. In the motivating example of the cache, you would still need to remove that nullish weak reference from your cache, but that’s a straightforward operation — memory leak solved.

let obj = {id: “The object to remember”, counter: 0};
let weakRef = new WeakRef(obj);

let strongReference = weakRef.deref();
if (strongReference != null) {
//use strongReference here
}

FinalizationRegistry

The FinalizationRegistry allows you to specify that a certain callback is called when a weak reference is reclaimed.

const registry = new FinalizationRegistry(heldValue => {
console.log(“registry called on: “+heldValue);
});
registry.register(obj, “held value”);

In the example above, the obj is registered, and once it is garbage collected, the callback is executed and the “held value” passed as argument so you can identify the object.

Logical Assignment Operators

There are three new logical assignment operators: the logical OR assignment operator ||= the logical AND assignment operator &&= and the logical nullish assignment operator ??=. They simplify code that previously needed an if, in order to achieve both a check AND retain short-circuiting behavior. Consider the following case:

val = (val || default);

This code ensures that the val variable has a valid entry: it checks whether val is available (truthy), and if not, assigns default to it. But that’s not entirely true. What this code effectively does is assign a value in any case; either the previous value of val is (re-)assigned, or the default value is. That doesn’t sound like a huge difference — and in practice it isn’t, since many JavaScript engines will be able to optimize away the difference — but there are cases where it can make a difference. For instance, consider val to be an accessor property, in which case it is evaluated twice: first, the getter is executed, the value retrieved, and then checked for truthyness. Even if it already IS truthy, the same value is assigned using the setter — with no practical effect, unless there are observable side effects in getter or setter. So what you rather want to do in practice might be the following, more clumsy variant:

if (!val) { 
val = default;
}

Or, with the identical effect but probably less readable:

val || (val = default);

Logical assignment operators now simplify that operation and make the operation more readable by combining the check and the assignment into one operation. Instead of the lines above, you can now just write the following:

val ||= default;

This checks val for truthyness, and only if it is falsy, assigns default to it; if val already is truthy, nothing more happens (i.e., the getter of val is only executed once).

Similarly, the logical AND assignment operator assigns only if the value is already truthy, and the logical nullish assignment operator only assigns if the value is nullish (null or undefined):

val &&= other; //equivalent to previous: val && (val = other);
val ??= other; //equivalent to previous: val ?? (val = other);

Numeric Separators

Finally, a very light but helpful topic: you can now use an underscore character _ as separator in numerical constants in your code. Conceptually, think of them as thousands separators without any effect on the semantic of the number — they just improve readability. Note that you are not limited to separating numbers into blocks of thousands; any separation is possible.

const million = 1_000_000;
const double = 1_234_567_890.123;
const fraction = 0.000_001;
const binary = 0b1010_0001_1000_0101;
const hex = 0xA0_B0_C0;

When parsing a number, the underscores are removed and the remaining number is parsed just as expected. You cannot start a number with an underscore (then it would be parsed as identifier), you cannot end a number with an underscore, and you can only have one consecutive underscore. This also applies to the decimal point — don’t neighbor a decimal point with an underscore. Or in other terms: each underscore used as a decimal separator strictly needs a digit on both sides.

Looking beyond ECMAScript 2021

Graal.js is implemented as an Abstract Syntax Tree (AST) interpreter. The Polyglot API makes this task rather easy, and it helps to still achieve excellent performance. By providing different specializations in the AST nodes, the interpreter can speculatively optimize itself during execution and thus optimize the performance. GraalVM itself will compile the AST interpreter similar to all other Java code and generate highly optimized machine code from the AST. Ultimately, that helps the Graal.js team to achieve two usually conflicting goals with just one implementation:

  • making the implementation and maintenance of features easy, as we just have to provide one implementation as AST nodes, BUT
  • still achieving excellent execution performance of the resulting engine, as it is JIT-compiled to machine code during execution.

In GraalVM, adopting new features of ECMAScript usually just requires straightforwardly expressing the abstract specification text in concrete AST nodes following the exact same steps as the specs — that already suffices to have a complete AND performant implementation.

Thanks to this simplicity, we have started to look into some of the upcoming features, or proposals as the TC39 calls them. Those proposals follow a 5 step process along 5 stages — from Stage 0 to Stage 4 (finished). All the features discussed above that landed in ECMAScript 2021 are in Stage 4. Below we will discuss some proposals that are in stage 2 or even 1. For some, this is pretty early in their evolution cycle and some will still undergo larger changes in semantics and syntax before being adopted — if they are ever accepted by the committee at all.

Operator Overloading

Operator overloading is a feature you might be aware of from languages like C. In JavaScript, the current proposal (currently still in Stage 1) offers a safe and clean way to provide operator overloading for classes only. While this is mostly “syntactic sugar,” it could simplify many programs and improve readability, while still maintaining sanity on the language level.

For GraalVM, operator overloading is especially promising when thinking about interoperability use-cases. GraalVM is ideal for sharing code and data across different programming languages, e.g., accessing a Java Map or a Python Array in JavaScript. Overloading operators would provide a nice way to specify some of the necessary conversions or tackle other problems in that context. Of course, bringing additional languages into the picture also complicates this effort a lot, as it opens up questions like, “how to interact with other languages that also allow overloading,” and “how to specify the overloading in this context,” etc.

For GraalVM, we have a complete implementation of the current version of the overloading aspects of the spec ready. However, there are still a few open questions that the current proposal does not answer in detail yet. For instance, to avoid conflicts with existing code, operator overloading will likely have to be enabled explicitly for any relevant block of code. One suggested solution for this is a new `with operators from` clause — Graal.js does not yet support this and enables (or disables) overloading globally at the moment.

Decorators

Decorators are similar to annotations in Java. They provide a way to declaratively attach annotations to classes, fields, and methods.

Unfortunately, the current state of the proposal is still expected to undergo some changes before reaching Stage 4 and acceptance. We have a branch open that implements the current state, but several changes will still land in the spec, so we cannot merge this currently. Once the spec authors have reached consensus on how to continue, we will update our implementation and be able to provide support for this pretty quickly.

This proposal has been implemented by a bachelor student at JKU Linz, that Oracle Labs is collaborating with. Thanks https://github.com/flohuemer for your effort!

Temporal

Temporal is a new date/time library. It solves the problems that the existing `Date` object in JavaScript has, and brings a large new API with great support for TimeZones and all kinds of abstractions for times, dates, and durations, etc. The proposal recently reached Stage 3, so it is expected to be accepted into the specification once all the requirements are fulfilled, like good test coverage, implementations in several engines, and a complete and polished specification text, etc.

GraalVM is currently working on implementing the full spec text. An incomplete preview version (with support for PlainTime, PlainDate, PlainDateTime, PlainYearMonth, and PlainMonthDay, as well as Calendar and Duration) is planned to be available in the GraalVM 21.2 release (behind the `js.temporal` flag), and full support is expected in 21.3.

Like the Decorators proposal, this feature is based on an implementation by a JKU bachelor student. A big shout out to https://github.com/korti11 for your implementation!

Conclusion

Graal.js is a complete and compatible implementation of the ECMAScript language specification, which supports the latest features of the ECMAScript 2021 specification. New features are added regularly, so when you use the latest version of GraalVM you can also try out many of the upcoming features of modern JavaScript. GraalVM is open-source, so if you are interested in contributing, we are happy to receive pull requests for ECMAScript proposals, other spec changes, or any other improvements. Let us know what you are working on!

--

--

Christian Wirth
graalvm

Manager with Oracle Labs, working on the Graal/JavaScript and TruffleRuby implementations. Opinions are my own.