Description
I discussed this a bit in the Matrix room first to get a temperature check. Apologies in advance for the large pile of text
Background
temporal_rs
I've been implementing Temporal in V8, using the temporal_rs
library, used by boa. Most of the implementation concerns here pertain to both V8 and Boa.
temporal_rs is a library whose API can be tweaked if necessary1, but overall I think its API is a good example of an attempt to split surface-level "JS interaction" code from the underlying spec logic/algorithms2. I think that this type of implementation hygeine is worth supporting as a specification, and if temporal_rs' model is incompatible with the spec, it's potentially a sign for a spec change.
WebIDL
(This section is not really a motivation, but is an attempt to explain the type of validation/parsing model I am aiming for)
I like the model WebIDL presents, providing a very clear distinction between "JS interaction" code and the underlying spec logic. Specifications using WebIDL seldom have to explicitly interact with JS beyond throwing errors; the JS layer is basically handled by the structure provided by WebIDL itself.
For example, Instant.since()
would typically have the "input types" all clearly specified in WebIDL, which would imply a bunch of pre-parsing and type validation and the actual specification would do things like validating whether the settings are compatible (e.g. Instant.since()
doesn't want you to use "day" units) and then performing the actual algorithm.
An example `Instant.since()` in WebIDL
typedef (Instant or string) InstantLike;
enum Unit {
"auto",
"years",
....
"nanoseconds",
};
enum RoundingMode {
"ceil",
...
"halfEven",
}
struct DifferenceSettings {
Unit? largestUnit;
Unit? smallestUnit;
long? roundingIncrement;
RoundingMode? roundingMode;
}
interface Instant {
Duration since(InstantLike other, DifferenceSetting settings);
}
In the runtime, all of this validation would be deterministically performed at the boundary of since
(throwing any errors necessary), and only then would the spec code run.
I do not wish to suggest this specification move over to WebIDL: I'm using WebIDL to illustrate a good way of thinking about the JS-to-spec layer that is consistently used in Web specifications.
A different perspective on all of this is separating out "type validation" from "value validation", though that usage of terminology may be too ambiguous to usefully convey an idea.
Options loading and observability
By and large, all of this is mostly relevant to the part of the spec that deals with options loading, like GetDifferenceSettings
and GetTemporalUnitValuedOption
.
The order in which operations are performed does matter; it is observable to external code if e.g. smallestUnit
is read before largestUnit
, or if smallestUnit
and largestUnit
are read before validating their values in case, say, smallestUnit
has an invalid value for the context (you could notice using a proxy or custom getter that largestUnit
was still read before the routine raised an exception).
The current specification mixes up options loading and validation. For example, in GetDifferenceSettings
, the steps are:
Step 1: not a step
Step 2: get field (unit)
Step 3: validate
Step 4-5: get field
Step 6: independent of other steps
Step 7: get field (unit)
Step 8: validate
Step 9-10: resolve defaults/auto
Step 11-13: validate
Furthermore, each "get field (unit)" performs unit group validation and default resolution on top of parsing. This is a fair amount of validation overall.
Status quo in different implementations
Firefox matches the current spec exactly. Boa uses temporal_rs and batches up "get field" and option variant parsing (into typed values), but defers further validation to later; as such it is currently spec non compliant.
I am working on the V8 implementation and it does a bit of both, but I'm rapidly hitting a hard tradeoff. If I eagerly validate as the spec wants me to, I end up doing a bunch of operations twice (since temporal_rs
will do them again), AND I risk performing non-idempotent operations multiple times (e.g. Instant.prototype.since
"inverts" the rounding mode, which you don't want to do twice). If I lazily validate, it is far easier and more efficient, but I am not spec compliant.
The "non idempotent operations get performed multiple times" is by far the largest risk in my view. Performance hits are suboptimal, as is the maintenance burden of more complex validation code being carried in two places, but the issue with non idempotent options can easily cause subtle bugs.
Proposal
I haven't fully investigated the situations where Temporal performs interspersed loading and validation, but my understanding is that it's basically around all of the options bags (there aren't too many of those) and some select enumerated options like "unit". They show up a lot, but it's all shared code.
I'd like to propose that we move in the direction of consistently performing load-and-parse operations (GetOption
) first, to get the entire options bag, and any further validation can be performed after the full options bag has been loaded.
This would mean e.g. GetTemporalUnitValuedOption
would become a simple three-line GetOption
call like GetTemporalOverflowOption
. Potentially with default-handling like GetTemporalOffsetOption
. There can be a separate ValidateTemporalUnitValuedOption
that does unit group validation/etc.
This would also mean that GetDifferenceSettings
would call GetFooOption
for each object field first, and only then perform parsing and default resolution steps.
I don't necessarily want to turn this into some type of blocker where the Temporal team must agree to fully change everything in this spec, I'm mostly looking for general assent that this is a good direction to move in, and individual PRs could be approved on their merits. I don't think this is a large endeavor, but I don't want to commit the Temporal team to this endeavor, especially in case I'm wrong about the size.
I suspect what will end up happening is that I will discover these cases during the process of implementation, and while doing so open PRs to the Temporal repo, but I am open to other ideas.
There's also probably some leeway on how things are sliced in individual cases; e.g. Unit could still be validated into unit groups by "splitting" it into a timeunit and dayunit set of types. I don't think that's necessary, people may disagree.
Footnotes
-
Not that I claim to speak for the Boa/temporal_rs team here! They may have a certain plan for how it is designed and not wish to change it in certain directions. ↩
-
I think there are, really, three layers to this, not just two. You have very surface level "JS interaction" code (going from JS types into algorithm types), then you have "orientation" where you're validating and just generally figuring out what you've been asked, and then you have the underlying algorithm.
servo-media
andwebxr
draw the line between the second two.temporal_rs
draws the line between the first two. ↩