Implementing the Temporal Proposal in LibJS
Published on 2021-11-29.Over the past five months we've implemented the Temporal stage 3 proposal in LibJS, the SerenityOS JavaScript engine. In this case, we is Idan, Luke, and yours truly.
We're just three functions (& a bunch of bugfixes 🐛) short of having the minimum implementation completed, so I figured I'd write down some thoughts about the whole process :^)
Motivation
After finishing my rewrite of the Object
implementation back in June, I was looking for another project to work on. I first discovered the Temporal proposal around the same time; and since we had already experimented with implementing proposals and not just the stage 4 ECMA-262 spec before, I decided that was going to be it.
commit 8269921212875b75c918bde5d318f0c5de152dac
Author: Linus Groh <mail@linusgroh.de>
Date: Tue Jul 6 19:14:47 2021 +0100
LibJS: Add the Temporal namespace object :^)
Currently empty, but we gotta start somewhere! This is the start of
implementing the Temporal proposal (currently stage 3).
I have decided to start a new subdirectory (Runtime/Temporal/) as well
as a new C++ namespace (JS::Temporal) for this so we don't have to
prefix all the files and classes with "Temporal" - there will be a lot.
https://tc39.es/proposal-temporal/
I can't speak for others, but Idan and Luke joined me shortly after; and it's been a team effort ever since :^)
OK, How Much Code?
$ git rev-parse --short HEAD
7a27ecc135
$ scc Userland/Libraries/LibJS/Runtime/Temporal
───────────────────────────────────────────────────────────────────────────────
Language Files Lines Blanks Comments Code Complexity
───────────────────────────────────────────────────────────────────────────────
C Header 34 2049 343 215 1491 6
C++ 34 17693 3700 5467 8526 1695
───────────────────────────────────────────────────────────────────────────────
Total 68 19742 4043 5682 10017 1701
───────────────────────────────────────────────────────────────────────────────
Estimated Cost to Develop (organic) $303,647
Estimated Schedule Effort (organic) 8.744090 months
Estimated People Required (organic) 3.085114
───────────────────────────────────────────────────────────────────────────────
Processed 996444 bytes, 0.996 megabytes (SI)
───────────────────────────────────────────────────────────────────────────────
What's Special
When we started working on Temporal, LibJS was just a little over a year old, and in that year we learned a lot about what works well for us, and what doesn't.
Here are a couple of things that didn't follow the status quo:
Code Structure
In LibJS all of the ECMAScript built-in functions and objects reside within the Runtime/
directory — clearly inspired by JavaScriptCore, as Andreas worked on WebKit for years. As mentioned in the commit message above, I decided to make a new subdirectory for Temporal specifically, and we ended up doing the same for Intl. In both cases it nicely matches the JS API, as both of them have namespace objects, and avoids polluting the top-level directory with even more files.
JSC did not end up doing the same, FWIW :^)
Additionally, everything is enclosed in a new C++ namespace, JS::Temporal
(we only had JS
so far: JS::Object
, JS::Value
, etc.):
- Within the
Temporal
namespace, objects are referred to by their name only, e.g.PlainDateTime
. This is clear enough in all cases. - Outside of that but within LibJS, we need to qualify the namespace, e.g.
Temporal::PlainDateTime
— just likeTemporal.PlainDate
in JS. - Outside of LibJS, e.g. in the
js(1)
REPL, the full namespace is required, e.g.JS::Temporal::PlainDateTime
.
Again, we also did the same for Intl, and would do it for any other large namespace objects added to the language specification.
Finally, we strictly put all functions in their respective files split up by spec sections. No special reason other than consistency and zero need for bikeshedding.
Spec comments
Shortly before all of this I really started to embrace using spec comments in the respective implementation code, i.e. literally copying the whole spec text into the implementation. Yes, seriously.
Looks like this:
// 2.3.4 SystemDateTime ( temporalTimeZoneLike, calendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-systemdatetime
ThrowCompletionOr<PlainDateTime*> system_date_time(GlobalObject& global_object, Value temporal_time_zone_like, Value calendar_like)
{
Object* time_zone;
// 1. If temporalTimeZoneLike is undefined, then
if (temporal_time_zone_like.is_undefined()) {
// a. Let timeZone be ! SystemTimeZone().
time_zone = system_time_zone(global_object);
}
// 2. Else,
else {
// a. Let timeZone be ? ToTemporalTimeZone(temporalTimeZoneLike).
time_zone = TRY(to_temporal_time_zone(global_object, temporal_time_zone_like));
}
// 3. Let calendar be ? ToTemporalCalendar(calendarLike).
auto* calendar = TRY(to_temporal_calendar(global_object, calendar_like));
// 4. Let instant be ! SystemInstant().
auto* instant = system_instant(global_object);
// 5. Return ? BuiltinTimeZoneGetPlainDateTimeFor(timeZone, instant, calendar).
return builtin_time_zone_get_plain_date_time_for(global_object, time_zone, *instant, *calendar);
}
We didn't always do that. In fact, the majority of LibJS's code is still without any spec annotations. It's useful for a number of reasons though, so I wanted to take it to the next level in Temporal — 100% of its code is annotated like this, and I'm not going back.
While the usefulness of // 9. Else,
specifically remains a source of debate within the SerenityOS community, we have started to also use this approach in parts of LibWeb, for example.
Initial Lack of Tests in Test262
For a while now we've used test262 to aid with testing our own implementation. Despite that, we still use and continue to expand our own test suite, which became especially important again for Temporal: a considerable amount of tests initially written for the polyfill only got merged on October 1. Before that, test coverage was really scarce, so we wrote plenty of tests ourselves:
$ git ls-files Userland/Libraries/LibJS/Tests/builtins/Temporal | wc -l
298
$ git grep 'test(' Userland/Libraries/LibJS/Tests/builtins/Temporal | wc -l
995
Test262 coverage of Temporal will be expanded further in the future, and currently contains a number of broken tests, so take this with a grain of salt: at the time of writing, we pass ~85% of their tests.
Exposure to the ECMAScript Proposal Process & TC39
In the past we've only ever taken finished, mostly stable specs & proposals for our implementation. Temporal still being Stage 3 meant that we eventually found, reported, and in some cases fixed more than a dozen issues with the specification itself, which we encountered during development (as well as relying on other implementors doing the same).
While most of the actual decision making for any normative changes happens within TC39 meetings, it was nice to get involved a little bit and see who is making the specs and how they do it :^)
What's not Special
What absolutely did not change was how we approached this project. As you might know, the only plan in the SerenityOS project is not having a plan, so while one of the spec's champions kindly pointed us to a dependency graph he made for the JSC implementation, we still ended up picking functions at random and implementing the missing Abstract Operations (AOs) on the fly.
Rinse, repeat, stop once all functions, and — by necessity — AOs are implemented.
Other than perhaps being slightly more consistent than other parts of LibJS, we still used the same language (C++), coding style, patterns (e.g. TRY()
), existing engine mechanisms (PrototypeObject
, ThrowCompletionOr
, & co.). It will all feel very familiar if you already know the rest of the codebase.
What's Next
There's still a lot of work to do! The spec requires support for a single named timezone (UTC
, numeric timezone offsets like +01:30
are a different story but part of the core functionality as well) and calendar (iso8601
). To be more more widely useful, we'll need support for other named timezones (e.g. Europe/London
) as well as calendars (e.g. gregory
, the full list can be found on MDN). These would be sourced from the IANA Time Zone Database and the Unicode Common Locale Data Repository (CLDR) respectively, we already heavily utilize the latter for various functionality in ECMA-402 (Intl).
Speaking of Intl, there's a bunch of changes to be made to integrate Temporal with Intl objects, e.g. Intl.DateTimeFormat
. Since we don't have that implemented yet, we didn't make those changes either. Just a day later, Tim opened a PR starting to implement exactly that. It's still based on the vanilla Intl spec, though.
Lastly, it's still a stage 3 proposal and will probably receive further small editorial and normative changes for several more months, which we're obviously keeping up to date with.
SpiderMonkey, JavaScriptCore, and V8 are all working on their implementations, and I imagine Temporal will eventually ship unflagged in the major browser engines sometime in 2022 — it's gonna be great. Until then, you can already use it in the SerenityOS Browser today :^)