Skip to content

Commit 3504027

Browse files
committed
Some changes based off the remaining review comments
1 parent 88c3b9b commit 3504027

File tree

1 file changed

+125
-83
lines changed

1 file changed

+125
-83
lines changed

blog/2025-06-15-temporal-impl-1.md

Lines changed: 125 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ description:
1212
authors: boa-dev
1313
---
1414

15-
This will be a series of posts primarily about implementing a new
16-
JavaScript feature in Rust, specifically the new date/time built-in:
17-
Temporal. We'll be going over general lessons and interesting design choices we've
18-
stumbled upon, as well as the crates supporting that implementation.
15+
Writing a JavaScript engine in Rust can seem like pretty daunting task
16+
to some. To provide some insight into how we implement JavaScript
17+
features, we will be going over implementing a JavaScript feature in
18+
Rust.
1919

20-
Why should you care? Well we are not only implementing it for
21-
JavaScript, but Rust as well ... more on that in a bit.
20+
More specifically, this will be the first in a series of posts primarily
21+
about implementing the new date/time built-in: Temporal. We'll be going
22+
over general lessons and interesting design choices we've stumbled upon,
23+
as well as the crates supporting that implementation.
24+
25+
Why should you care? Well we are not only implementing Temporal for
26+
JavaScript, but for Rust as well ... more on that in a bit.
2227

2328
First, an aside!
2429

@@ -50,8 +55,8 @@ documentation][mdn-temporal].
5055
Being Boa a JavaScript engine / interpreter, developing a correct
5156
implementation of the ECMAScript specification is our raison d'être.
5257
This, in consequence, makes implementing Temporal one of our most
53-
important goals, since it represents roughly 7-8% of the
54-
current conformance test suite (~4000 of the ~50,000 tests).
58+
important goals, since it represents roughly 7-8% of the current
59+
conformance test suite (~4000 of the ~50,000 tests).
5560

5661
When the PR of the first prototype of Temporal for Boa was submitted, a
5762
few things became evident:
@@ -60,38 +65,40 @@ few things became evident:
6065
2. There's room for optimization and improvement
6166
3. This would be handy to have in Rust
6267

63-
So after the prototype was merged, we pulled it out of Boa's
64-
internal builtins and externalized into its own crate,
65-
[`temporal_rs`][temporal-rs-repo], which then first landed behind an
66-
experimental flag in Boa v0.18.
68+
So after the prototype was merged, we pulled it out of Boa's internal
69+
builtins and externalized into its own crate,
70+
[`temporal_rs`][temporal-rs-repo], which landed behind an experimental
71+
flag in Boa v0.18.
6772

68-
After over a year and a half of development, Boa now sits at a conformance
69-
of about 90% for Temporal (and growing), with the entire implementation
70-
being backed by `temporal_rs`.
73+
After over a year and a half of development, Boa now sits at a
74+
conformance of about 90% for Temporal (and growing), with the entire
75+
implementation being backed by `temporal_rs`.
7176

7277
For its part, `temporal_rs` is shaping up to be a proper Rust date/time
73-
library that can be used to implement Temporal in a JavaScript engine, and even support general date/time use cases.
78+
library that can be used to implement Temporal in a JavaScript engine,
79+
and even support general date/time use cases.
7480

7581
Let's take a look at Temporal: it's JavaScript API, it's Rust API in
7682
`temporal_rs`, and how `temporal_rs` supports implementing the
7783
specification.
7884

7985
## Important core differences
8086

81-
Let's briefly talk about JavaScript values (`JsValue`). This is
82-
functionally the core `any` value type of JavaScript. A `JsValue` could
83-
be a number represented as a 64 bit floating point, a string, a boolean,
84-
or an object. Not only is it an `any`, but `JsValue` is ultimately engine
85-
defined with various implementations existing across engines.
87+
First, we need to talk about JavaScript values (`JsValue`) for a bit.
88+
This is functionally the core `any` value type of JavaScript. A
89+
`JsValue` could be a number represented as a 64 bit floating point, a
90+
string, a boolean, or an object. Not only is it an `any`, but `JsValue`
91+
is ultimately engine defined with various implementations existing
92+
across engines.
8693

8794
While this is handy for a dynamically typed language like JavaScript, it
8895
is not ideal for implementing deep language specifications where an
8996
object or string may need to be cloned. Furthermore, it's just not great
9097
for an API in a typed language like Rust.
9198

9299
To work around this, we routinely use `FromStr` and a `FiniteF64` custom
93-
primitive to handle casting and constraining, respectively, which
94-
glues dynamic types like `JsValue` with a typed API.
100+
primitive to handle casting and constraining, respectively, which glues
101+
dynamic types like `JsValue` with a typed API.
95102

96103
For instance, in Boa, we heavily lean into using the below patterns:
97104

@@ -181,10 +188,10 @@ use temporal_rs::PlainDate;
181188
let plain_date = PlainDate::try_new_iso(2025, 6, 9)?;
182189
```
183190

184-
Interestingly enough, the `_iso` constructors are actually extensions of
185-
Temporal specification to provide a similar API in Rust. This is because
186-
the `_iso` constructors are assumed to exist due to resolving an
187-
`undefined` calendar to the default ISO calendar.
191+
Interestingly enough, the `_iso` constructors are mostly expressing a
192+
part of the JavaScript API, just in native Rust. This is because in
193+
JavaScript the `_iso` constructors are assumed to exist due to resolving
194+
an `undefined` calendar to the default ISO calendar.
188195

189196
## Let's discuss `Now`
190197

@@ -203,9 +210,9 @@ the `_iso` constructors are assumed to exist due to resolving an
203210
important. It is the object from which the current instant can be
204211
measured and mapped into any of the Temporal components.
205212

206-
In JavaScript, this type has no `[[Construct]]` or `[[Call]]` internal
207-
method, which is a fancy way to say that Now has no constructor and
208-
cannot be called directly.
213+
In JavaScript, this type has no [`[[Construct]]`][construct-link] or
214+
[`[[Call]]`][call-link] internal method, which is a fancy way to say
215+
that Now has no constructor and cannot be called directly.
209216

210217
Instead, Now is used primarily as a namespace for its methods.
211218

@@ -233,8 +240,9 @@ on the implementation, right?
233240

234241
Except the core purpose of `temporal_rs` is that it can be used in any
235242
engine implementation, and accessing a system clock and system time zone
236-
is sometimes difficult for engines that support targets like embedded systems.
237-
Thus, this functionality must be delegated to the engine or runtime ... somehow.
243+
is sometimes difficult for engines that support targets like embedded
244+
systems. Thus, this functionality must be delegated to the engine or
245+
runtime ... somehow.
238246

239247
How did we end up implementing `Now` if we have no access to the system
240248
clock or time zone? Well ... a builder pattern of course!
@@ -273,6 +281,34 @@ pub struct Now {
273281

274282
Once we've constructed `Now`, then we are off to the races!
275283

284+
In Boa, implementing `Now` is as easy the below implementation for
285+
`Temporal.Now.plainDateISO()`:
286+
287+
```rust
288+
impl Now {
289+
// The `Temporal.Now.plainDateISO` used when building `Temporal.Now`.
290+
fn plain_date_iso(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
291+
let time_zone = args
292+
.get_or_undefined(0)
293+
.map(|v| to_temporal_timezone_identifier(v, context))
294+
.transpose()?;
295+
296+
let now = build_now(context)?;
297+
298+
let pd = now.plain_date_iso_with_provider(time_zone, context.tz_provider())?;
299+
create_temporal_date(pd, None, context).map(Into::into)
300+
}
301+
}
302+
303+
// A helper for building Now
304+
fn build_now(context: &mut Context) -> JsResult<NowInner> {
305+
Ok(NowBuilder::default()
306+
.with_system_zone(system_time_zone()?)
307+
.with_system_nanoseconds(system_nanoseconds(context)?)
308+
.build())
309+
}
310+
```
311+
276312
The nice part about this approach is that it also allows a `std`
277313
implementation that can be feature gated for general users that are not
278314
concerned with `no_std`.
@@ -286,12 +322,12 @@ concerned with `no_std`.
286322
## Partial API
287323

288324
There's an interesting method on each of the Temporal built-ins that I'd
289-
assume most people who have used Rust would be familiar with: `from`. But
290-
this isn't Rust's friendly `From` trait. No, this `from` is a behemoth
291-
method that takes a `JsValue` and automagically gives you back the
292-
built-in that you'd like or throws. That's right! Give it a string, give
293-
it a property bag, give it an instance of another Temporal built-in;
294-
`from` will figure it out for you!
325+
assume most people who have used Rust would be familiar with: `from`.
326+
But this isn't Rust's friendly `From` trait. No, this `from` is a
327+
behemoth method that takes a `JsValue` and automagically gives you back
328+
the built-in that you'd like or throws. That's right! Give it a string,
329+
give it a property bag, give it an instance of another Temporal
330+
built-in; `from` will figure it out for you!
295331

296332
Simple, right?
297333

@@ -300,12 +336,12 @@ that! ... or at least not in that shape.
300336

301337
Again, the goal of `temporal_rs` is to implement the specification to
302338
the highest possible degree of conformance, so when we couldn't provide
303-
a direct translation of the specification's API, we made sure to
304-
provide APIs that (hopefully) made the glue code between engines and
339+
a direct translation of the specification's API, we made sure to provide
340+
APIs that (hopefully) made the glue code between engines and
305341
`temporal_rs` much shorter.
306342

307-
To exemplify this, let's take a look at some valid uses of `from` in JavaScript to
308-
construct a `PlainDate`.
343+
To exemplify this, let's take a look at some valid uses of `from` in
344+
JavaScript to construct a `PlainDate`.
309345

310346
```js
311347
// Create a `PlainDateTime`
@@ -322,23 +358,24 @@ const pd_from_property_bag = Temporal.PlainDate.from({
322358
});
323359
```
324360

325-
If we look closely to the common usage of the method, it seems like
326-
all that needs to be implemented by `temporal_rs` is:
361+
If we look closely to the common usage of the method, it seems like all
362+
that needs to be implemented by `temporal_rs` is:
363+
327364
- `From<PlainDateTime>`: Easy.
328365
- `From<ZonedDateTime>`: Simple.
329366
- `FromStr`: Tricky but can be done.
330-
- `From<JsObject>`: ...
331-
... oh. Did I mention `JsObject`, like `JsValue`, is engine defined as well?
367+
- `From<JsObject>`: ... ... oh. Did I mention `JsObject`, like
368+
`JsValue`, is engine defined as well?
332369

333370
Fortunately, this is where `temporal_rs`'s Partial API comes in.
334371

335372
It turns out that, while property bags in JavaScript can have various
336373
fields set, there is still a general shape for the fields that can be
337374
provided and validated in Temporal.
338375

339-
To support this in `temporal_rs`, a "partial" component
340-
exists for each of the components that can then be provided to that
341-
component's `from_partial` method.
376+
To support this in `temporal_rs`, a "partial" component exists for each
377+
of the components that can then be provided to that component's
378+
`from_partial` method.
342379

343380
With this, we have fully implemented support for the `from` method in
344381
`temporal_rs`:
@@ -352,12 +389,12 @@ let pd_from_pdt = PlainDate::from(pdt);
352389
// We can use a `str`.
353390
let pd_from_string = PlainDate::from_str("2025-01-01")?;
354391
// We can use a `PartialDate`.
355-
let pd_from_partial = PlainDate::from_partial(PartialDate {
356-
year: Some(2025),
357-
month: Some(1),
358-
day: Some(1),
359-
..Default::default()
360-
});
392+
let pd_from_partial = PlainDate::from_partial(
393+
PartialDate::new()
394+
.with_year(Some(2025))
395+
.with_month(Some(1))
396+
.with_day(Some(1))
397+
);
361398
```
362399

363400
**NOTE:** there may be updates to `PartialDate` in the future (see
@@ -368,8 +405,8 @@ for more information).
368405

369406
So far we have not discussed time zones, and -- surprise! -- we aren't
370407
going to ... yet. It's not because they aren't super cool and
371-
interesting and everyone _totally_ 100% loves them. No, time zones aren't
372-
in this post because they are still being polished and deserve an
408+
interesting and everyone _totally_ 100% loves them. No, time zones
409+
aren't in this post because they are still being polished and deserve an
373410
entire post of their own.
374411

375412
So stay tuned for our next post on implementing Temporal! The one where
@@ -384,16 +421,14 @@ In conclusion, we're implementing Temporal in Rust to support engine
384421
implementors as well as to have the API available in native Rust in
385422
general.
386423

387-
Boa currently sits at a [90% conformance rate][boa-test262] for Temporal
388-
completely backed by `temporal_rs` v0.0.8, and we're aiming to be 100%
389-
conformant before the end of the year.
390-
391424
If you're interested in trying Temporal using Boa, you can use it in
392425
Boa's CLI or enable it in `boa_engine` with the `experimental` flag.
393426

394427
Outside of Boa's implementation, `temporal_rs` has implemented or
395428
supports the implementation for a large portion of the Temporal's API in
396-
native Rust.
429+
native Rust. Furthermore, an overwhelming amount of the API can be
430+
considered stable[^stability] and is currently available in Boa with
431+
only a few outstanding issues that may be considered breaking changes.
397432

398433
If you're interested in trying out `temporal_rs`, feel free to add it to
399434
your dependencies with the command:
@@ -402,40 +437,47 @@ your dependencies with the command:
402437
cargo add temporal_rs
403438
```
404439

405-
or by adding the below in the `[dependencies]` section of your `Cargo.toml`:
440+
or by adding the below in the `[dependencies]` section of your
441+
`Cargo.toml`:
406442

407443
```toml
408444
temporal_rs = "0.0.9"
409445
```
410446

411447
A FFI version of temporal is also available for C and C++ via
412-
`temporal_capi`.
448+
[`temporal_capi`][temporal-capi].
413449

414-
## General note on API stability
450+
[^stability]: A general note on API stability
415451

416-
While the majority of the APIs discussed above are expected to be mostly
417-
stable. Temporal is still a stage 3 proposal that is not fully accepted
418-
into the ECMAScript specification. Any normative change that may be made
419-
upstream in the ECMAScript or ECMA402 specification will also be
420-
reflected in `temporal_rs`.
452+
While the majority of the APIs discussed above are expected to be
453+
mostly stable. Temporal is still a stage 3 proposal that is not
454+
fully accepted into the ECMAScript specification. Any normative
455+
change that may be made upstream in the ECMAScript or ECMA402
456+
specification will also be reflected in `temporal_rs`.
421457

422-
There are also a few outstanding issues with changes that may reflect in
423-
the API.
458+
There are also a few outstanding issues with changes that may
459+
reflect in the API.
424460

425-
1. Duration's inner repr and related constructors.
426-
2. TemporalError's inner repr
427-
3. Partial objects may need some adjustments to handle differences
428-
between `from_partial` and `with`
429-
4. Time zone provider's and the `TimeZoneProvider` trait are still
430-
largely unstable. Although, the provider APIs that use them are
431-
expected to be stable (spoilers!)
432-
5. Era and month code are still be discussed in the intl-era-month-code
433-
proposal, so some calendars and calendar methods may have varying
434-
levels of support.
461+
1. Duration's inner repr and related constructors.
462+
2. `ZonedDateTime.prototype.getTimeZoneTransition` implementation
463+
3. TemporalError's inner repr
464+
4. Partial objects may need some adjustments to handle differences
465+
between `from_partial` and `with`
466+
5. Time zone provider's and the `TimeZoneProvider` trait are still
467+
largely unstable. Although, the provider APIs that use them are
468+
expected to be stable (spoilers!)
469+
6. Era and month code are still be discussed in the
470+
intl-era-month-code proposal, so some calendars and calendar
471+
methods may have varying levels of support.
435472

436-
The above issues are considered blocking for a 0.1.0 release.
473+
The above issues are considered blocking for a 0.1.0 release.
437474

438475
[mdn-temporal]:
439476
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
440477
[temporal-rs-repo]: https://github.com/boa-dev/temporal
478+
[construct-link]:
479+
https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ecmascript-function-objects-construct-argumentslist-newtarget
480+
[call-link]:
481+
https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ecmascript-function-objects-call-thisargument-argumentslist
441482
[boa-test262]: https://test262.fyi/#|boa
483+
[temporal-capi]: https://crates.io/crates/temporal_capi

0 commit comments

Comments
 (0)