@@ -12,13 +12,18 @@ description:
12
12
authors : boa-dev
13
13
---
14
14
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 .
19
19
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.
22
27
23
28
First, an aside!
24
29
@@ -50,8 +55,8 @@ documentation][mdn-temporal].
50
55
Being Boa a JavaScript engine / interpreter, developing a correct
51
56
implementation of the ECMAScript specification is our raison d'être.
52
57
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).
55
60
56
61
When the PR of the first prototype of Temporal for Boa was submitted, a
57
62
few things became evident:
@@ -60,38 +65,40 @@ few things became evident:
60
65
2 . There's room for optimization and improvement
61
66
3 . This would be handy to have in Rust
62
67
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.
67
72
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 ` .
71
76
72
77
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.
74
80
75
81
Let's take a look at Temporal: it's JavaScript API, it's Rust API in
76
82
` temporal_rs ` , and how ` temporal_rs ` supports implementing the
77
83
specification.
78
84
79
85
## Important core differences
80
86
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.
86
93
87
94
While this is handy for a dynamically typed language like JavaScript, it
88
95
is not ideal for implementing deep language specifications where an
89
96
object or string may need to be cloned. Furthermore, it's just not great
90
97
for an API in a typed language like Rust.
91
98
92
99
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.
95
102
96
103
For instance, in Boa, we heavily lean into using the below patterns:
97
104
@@ -181,10 +188,10 @@ use temporal_rs::PlainDate;
181
188
let plain_date = PlainDate :: try_new_iso (2025 , 6 , 9 )? ;
182
189
```
183
190
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.
188
195
189
196
## Let's discuss ` Now `
190
197
@@ -203,9 +210,9 @@ the `_iso` constructors are assumed to exist due to resolving an
203
210
important. It is the object from which the current instant can be
204
211
measured and mapped into any of the Temporal components.
205
212
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.
209
216
210
217
Instead, Now is used primarily as a namespace for its methods.
211
218
@@ -233,8 +240,9 @@ on the implementation, right?
233
240
234
241
Except the core purpose of ` temporal_rs ` is that it can be used in any
235
242
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.
238
246
239
247
How did we end up implementing ` Now ` if we have no access to the system
240
248
clock or time zone? Well ... a builder pattern of course!
@@ -273,6 +281,34 @@ pub struct Now {
273
281
274
282
Once we've constructed ` Now ` , then we are off to the races!
275
283
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
+
276
312
The nice part about this approach is that it also allows a ` std `
277
313
implementation that can be feature gated for general users that are not
278
314
concerned with ` no_std ` .
@@ -286,12 +322,12 @@ concerned with `no_std`.
286
322
## Partial API
287
323
288
324
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!
295
331
296
332
Simple, right?
297
333
@@ -300,12 +336,12 @@ that! ... or at least not in that shape.
300
336
301
337
Again, the goal of ` temporal_rs ` is to implement the specification to
302
338
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
305
341
` temporal_rs ` much shorter.
306
342
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 ` .
309
345
310
346
``` js
311
347
// Create a `PlainDateTime`
@@ -322,23 +358,24 @@ const pd_from_property_bag = Temporal.PlainDate.from({
322
358
});
323
359
```
324
360
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
+
327
364
- ` From<PlainDateTime> ` : Easy.
328
365
- ` From<ZonedDateTime> ` : Simple.
329
366
- ` 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?
332
369
333
370
Fortunately, this is where ` temporal_rs ` 's Partial API comes in.
334
371
335
372
It turns out that, while property bags in JavaScript can have various
336
373
fields set, there is still a general shape for the fields that can be
337
374
provided and validated in Temporal.
338
375
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.
342
379
343
380
With this, we have fully implemented support for the ` from ` method in
344
381
` temporal_rs ` :
@@ -352,12 +389,12 @@ let pd_from_pdt = PlainDate::from(pdt);
352
389
// We can use a `str`.
353
390
let pd_from_string = PlainDate :: from_str (" 2025-01-01" )? ;
354
391
// 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
+ );
361
398
```
362
399
363
400
** NOTE:** there may be updates to ` PartialDate ` in the future (see
@@ -368,8 +405,8 @@ for more information).
368
405
369
406
So far we have not discussed time zones, and -- surprise! -- we aren't
370
407
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
373
410
entire post of their own.
374
411
375
412
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
384
421
implementors as well as to have the API available in native Rust in
385
422
general.
386
423
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
-
391
424
If you're interested in trying Temporal using Boa, you can use it in
392
425
Boa's CLI or enable it in ` boa_engine ` with the ` experimental ` flag.
393
426
394
427
Outside of Boa's implementation, ` temporal_rs ` has implemented or
395
428
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.
397
432
398
433
If you're interested in trying out ` temporal_rs ` , feel free to add it to
399
434
your dependencies with the command:
@@ -402,40 +437,47 @@ your dependencies with the command:
402
437
cargo add temporal_rs
403
438
```
404
439
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 ` :
406
442
407
443
``` toml
408
444
temporal_rs = " 0.0.9"
409
445
```
410
446
411
447
A FFI version of temporal is also available for C and C++ via
412
- ` temporal_capi ` .
448
+ [ ` temporal_capi ` ] [ temporal-capi ] .
413
449
414
- ## General note on API stability
450
+ [ ^ stability ] : A general note on API stability
415
451
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 ` .
421
457
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.
424
460
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.
435
472
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.
437
474
438
475
[ mdn-temporal] :
439
476
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
440
477
[ 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
441
482
[ boa-test262 ] : https://test262.fyi/#|boa
483
+ [ temporal-capi ] : https://crates.io/crates/temporal_capi
0 commit comments