Skip to content

feat(temporal): Limited support for Temporal#1416

Open
nabetti1720 wants to merge 4 commits intoawslabs:mainfrom
nabetti1720:feat/temporal
Open

feat(temporal): Limited support for Temporal#1416
nabetti1720 wants to merge 4 commits intoawslabs:mainfrom
nabetti1720:feat/temporal

Conversation

@nabetti1720
Copy link
Contributor

@nabetti1720 nabetti1720 commented Feb 24, 2026

Issue # (if available)

n/a

Description of changes

  • This PR will implement Temporal as best as possible, taking full advantage of jiff's capabilities.
  • The core functionality is based on jiff, and llrt_temporal is merely a very thin wrapper to conform to the Temporal specification.
  • In the future, we may consider using temporal_rs to have calendar functionality. However, it would be even better if we could switch to it in the backend for use cases that don't require a calendar, but that's not the goal of this PR.
  • For full support, including the calendar, we will consider using icu and jiff-icu (but not in this PR). If we do, we may place them after the feature gate due to the increased footprint.
    https://github.com/BurntSushi/jiff/blob/master/COMPARE.md#icu-v150

Not included in this PR (postponed to next time)

  • Implement of Temporal.Instant.round()
  • Implement of Temporal.Duration.round()
  • Implement of Temporal.Duration.total()
  • Implement of Temporal.ZoneDateTime.round()
  • Temporal support for Intl.DateTimeFormat()

Checklist

  • Created unit tests in tests/unit and/or in Rust for my feature if needed
  • Ran make fix to format JS and apply Clippy auto fixes
  • Made sure my code didn't add any additional warnings: make check
  • Added relevant type info in types/ directory
  • Updated documentation if needed (API.md/README.md/Other)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@nabetti1720 nabetti1720 force-pushed the feat/temporal branch 4 times, most recently from b4c9989 to 000ee83 Compare February 24, 2026 13:22
@nabetti1720 nabetti1720 force-pushed the feat/temporal branch 22 times, most recently from b1505fd to 3ec136d Compare February 28, 2026 12:57
@nabetti1720 nabetti1720 marked this pull request as ready for review February 28, 2026 13:21
@nabetti1720
Copy link
Contributor Author

@richarddavison Please take a look when you have time. :)

@Sytten
Copy link
Collaborator

Sytten commented Feb 28, 2026

The choice of jiff is interesting vs chrono or time (which we probably already have in the tree)

@nabetti1720
Copy link
Contributor Author

nabetti1720 commented Feb 28, 2026

The choice of jiff is interesting vs chrono or time (which we probably already have in the tree)

chrono and chrono-tz, which existed in this tree, was replaced by jiff in #1385.

I really like jiff because of its affinity with the Temporal API. This library allows us to focus solely on the interface between rquickjs and jiff.

Here is some interesting documentation about jiff:

@nabetti1720
Copy link
Contributor Author

@Sytten It would be great to optimize further if RquickJS supported seamless BigInt<->i128 conversion. Is there a good way to go about this?

@Sytten
Copy link
Collaborator

Sytten commented Mar 1, 2026

Make sense! I will have to check it, it is still niche for now so limited support in other libs. Not a fan that it has not seen a release in 1y+

As for BigInt the C API is limited an f64 as far as O remember. You can try to add an API in rquickjs but it will likely need to be added on the C side (guys are pretty responsive though).

@nabetti1720

This comment was marked as off-topic.

@nabetti1720
Copy link
Contributor Author

We've added a few more supportable properties to ZonedDateTime. And that's really it.

Copy link
Collaborator

@richarddavison richarddavison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for the PR, some minor comments and questions to address other wise looks good!

self.map_err(|err| Exception::throw_message(ctx, &err.to_string()))
}

fn map_err_range(self, ctx: &Ctx) -> Result<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or_throw_range maybe for consistency?

Copy link
Contributor Author

@nabetti1720 nabetti1720 Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or_throw() is a method that unconditionally specifies the msg of the caller, but I wanted a shorthand equivalent for other error types.

Ideally, we would haveor_throw_msg (alias as or_throw), or_throw_type and or_throw_range, but they are already defined.

pub trait ResultExt<T> {
    fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result<T>;
    fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result<T>;
    fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result<T>;
    fn or_throw(self, ctx: &Ctx) -> Result<T>;
}

I'd like to refactor it to the following, what do you think?

pub enum By {
    Internal,
    Message,
    Range,
    Reference,
    Syntax,
    Type,
}

pub trait ResultExt<T> {
    fn or_throw_msg_into_msg(self, ctx: &Ctx, msg: &str) -> Result<T>;
    fn or_throw_range_into_msg(self, ctx: &Ctx, msg: &str) -> Result<T>;
    fn or_throw_type_into_msg(self, ctx: &Ctx, msg: &str) -> Result<T>;
    fn or_throw(self, ctx: &Ctx) -> Result<T>;
    fn or_throw_by(self, ctx: &Ctx, by: By) -> Result<T>;
}

Since it is used so frequently, I think it is necessary to aim for a description method that is as short as possible.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't get why we can't use the regular :

fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result<T>;
fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result<T>;
fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result<T>;
fn or_throw(self, ctx: &Ctx) -> Result<T>;

What's missing? Why do we need the additional methods?

Copy link
Contributor Author

@nabetti1720 nabetti1720 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    fn or_throw(self, ctx: &Ctx) -> Result<T> {
        self.map_err(|err| Exception::throw_message(ctx, &err.to_string()))
    }

We need a shorthand method like or_throw() that will throw another Exception while still printing the internal message of the error object.

    fn or_throw_by(self, ctx: &Ctx, by: By) -> Result<T> {
        match by {
            By::Internal => self.map_err(|err| Exception::throw_internal(ctx, &err.to_string())),
            By::Message => self.map_err(|err| Exception::throw_message(ctx, &err.to_string())),
            By::Range => self.map_err(|err| Exception::throw_range(ctx, &err.to_string())),
            By::Reference => self.map_err(|err| Exception::throw_reference(ctx, &err.to_string())),
            By::Syntax => self.map_err(|err| Exception::throw_syntax(ctx, &err.to_string())),
            By::Type => self.map_err(|err| Exception::throw_type(ctx, &err.to_string())),
        }
    }

Copy link
Contributor Author

@nabetti1720 nabetti1720 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or_throw_range(&ctx, "") or or_throw_type(&ctx, "") would also work. It's a bit hacky, but I'll fix it if you prefer.

EDIT: This is a bit off topic, but jiff's error messages are very strict and clear, so there's no reason not to take advantage of them.

% cat reproduction.js 
const zdt = Temporal.ZonedDateTime.from("2021-07-01T12:00:00-05:00[America/New_York]");
console.log(zdt);

% llrt reproduction.js
RangeError: datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-05`, but the time zone `America/New_York` for the given datetime unambiguously has offset `-04`
  at <anonymous> (/Users/shinya/Workspaces/llrt-test/reproduction.js:1:35)

Comment on lines +53 to +55
if let Some(str) = info.as_string().and_then(|s| s.to_string().ok()) {
return Self::from_str(&ctx, &str);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we borrow the JS string to avoid to_string call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at other use cases for LLRT, but I couldn't find anything other than converting to the (Standard) String type using as_string() -> to_string(). Is there any good way to do this?

Copy link
Contributor Author

@nabetti1720 nabetti1720 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found an implementation like this, but does it fit your needs?

let js_string = unsafe { value.as_string().unwrap_unchecked() };
let mut len = mem::MaybeUninit::uninit();
let ctx_ptr = js_string.ctx().as_raw().as_ptr();
let ptr =
unsafe { qjs::JS_ToCStringLen(ctx_ptr, len.as_mut_ptr(), js_string.as_raw()) };
if ptr.is_null() {
return Err(Error::Unknown);
}
let len = unsafe { len.assume_init() };
let bytes: &[u8] = unsafe { slice::from_raw_parts(ptr as _, len as _) };
let raw_string = unsafe { str::from_utf8_unchecked(bytes) };
write_string(context.result, raw_string);
unsafe { qjs::JS_FreeCString(js_string.ctx().as_raw().as_ptr(), ptr) };

The rquickjs itself has a similar implementation, so I'm not sure how much of an advantage there is in using methods other than to_string().

rquickjs-core/src/value/string.rs:

    /// Convert the JavaScript string to a Rust string.
    pub fn to_string(&self) -> Result<StdString> {
        let mut len = mem::MaybeUninit::uninit();
        let ptr = unsafe {
            qjs::JS_ToCStringLen(self.0.ctx.as_ptr(), len.as_mut_ptr(), self.0.as_js_value())
        };
        if ptr.is_null() {
            // Might not ever happen but I am not 100% sure
            // so just incase check it.
            return Err(Error::Unknown);
        }
        let len = unsafe { len.assume_init() };
        let bytes: &[u8] = unsafe { slice::from_raw_parts(ptr as _, len as _) };
        let result = str::from_utf8(bytes).map(|s| s.into());
        unsafe { qjs::JS_FreeCString(self.0.ctx.as_ptr(), ptr) };
        Ok(result?)
    }

Comment on lines +116 to +121
fn value_of(&self, ctx: Ctx<'_>) -> Result<()> {
Err(Exception::throw_type(
&ctx,
"can't convert Instant to primitive type",
))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have a method that just throws?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a requirement specification.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/valueOf

Because both primitive conversion and number conversion call valueOf() before toString(), if valueOf() is absent, then an expression like instant1 > instant2 would implicitly compare them as strings, which may have unexpected results. By throwing a TypeError, Temporal.Instant instances prevent such implicit conversions. You need to explicitly convert them to numbers using Temporal.Instant.prototype.epochNanoseconds, or use the Temporal.Instant.compare() static method to compare them.

@nabetti1720
Copy link
Contributor Author

@richarddavison I think I've fixed most of the issues, but I'd like some further feedback on some of them. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants