Skip to content

Conversation

@sebastianjacmatt
Copy link

No description provided.

sebastianjacmatt and others added 28 commits November 27, 2025 12:04
…ntPrototype%Temporal.Instant.Prototype% as well as heap data and handles
Copy link
Member

@aapoalas aapoalas left a comment

Choose a reason for hiding this comment

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

A partial review.

weak-refs = []
set = []
typescript = []
temporal = ["temporal_rs"]
Copy link
Member

Choose a reason for hiding this comment

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

issue: Feature is doubled.

/// an integer or a throw completion.
/// It converts argument to an integer representing its Number value,
/// or throws a RangeError when that value is not integral.
pub(crate) fn to_integer_if_integral<'gc>(
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: Should be gated on the feature flag.

if let Value::Integer(digits_value) = digits_value
&& (0..=9).contains(&digits_value.into_i64())
{
return Ok(temporal_rs::parsers::Precision::Digit(
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: Maybe add a small comment regarding this fast path.

// b. Return auto.
return Ok(temporal_rs::parsers::Precision::Auto);
}
// SAFETY: not shared.
Copy link
Member

Choose a reason for hiding this comment

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

issue: You should be throwing an error here. "If not 'auto', throw a RangeError exception. Else, return 'auto'."

));
}
// 3. If digitsValue is not a Number, then
if !digits_value.is_number() {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion:

Suggested change
if !digits_value.is_number() {
let Ok(digits_value) = Number::try_from(digits_value) else {

This should work to convert digitsValue into a number after this if branch; you just need to add the currently missing "throw RangeError" part inside the else.

}
// 3. If digitsValue is not a Number, then
if !digits_value.is_number() {
let scoped_digits_value = digits_value.scope(agent, gc.nogc());
Copy link
Member

Choose a reason for hiding this comment

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

note: This scoping becomes unnecessary.

));
}
// 5. Let digitCount be floor(ℝ(digitsValue)).
let digit_count = digits_value
Copy link
Member

Choose a reason for hiding this comment

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

issue: ToNumber here is extra. Spec's R(digitsValue) means effectively digits_value.to_real(agent) which is equivalent to into_f64.


impl DurationHeapData<'_> {
pub fn default() -> Self {
todo!()
Copy link
Member

Choose a reason for hiding this comment

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

issue: This is either dead code or will crash the program when Temporal.Duration is used :)

Copy link
Author

Choose a reason for hiding this comment

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

Should be fixed now, along with Duration constructor and AO's.

As a side note on Duration implementation, I've borrowed implementation from Instant.create_temporal_instant in:

fn create_temporal_duration

unsafe { object.set_duration(agent, duration) };

where set_duration is defined as

/// # Safety
///
/// Should be only called once; reinitialising the value is not allowed.
unsafe fn set_duration(self, agent: &mut Agent, duration: temporal_rs::Duration) {
    agent[self].duration = duration;
}

Is this safe?

/// or a throw completion. It reads unit and rounding options needed by difference operations.
pub(crate) fn get_difference_settings<'gc, const IS_UNTIL: bool>(
agent: &mut Agent,
options: Object<'gc>, // options (an Object)
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: No reason for the 'gc lifetime here.

Suggested change
options: Object<'gc>, // options (an Object)
options: Object, // options (an Object)

mut gc: GcScope<'gc, '_>,
) -> JsResult<'gc, DifferenceSettings> {
let _unit_group = _unit_group.bind(gc.nogc());
let _disallowed_units = _disallowed_units.bind(gc.nogc());
Copy link
Member

Choose a reason for hiding this comment

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

note: Binding all of these arguments is entirely unnecessary since they do not have any lifetimes.

@@ -0,0 +1,488 @@
pub(crate) mod data;
Copy link
Member

Choose a reason for hiding this comment

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

issue: Missing MPLv2 headers.

Copy link
Member

@aapoalas aapoalas left a comment

Choose a reason for hiding this comment

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

Alright, lots of good code but also a lot of little corners to file and polish.

But overall, this is quite good as-is already, so excellent work! There weren't any big issues, no overt GC lifetime problems that I detected, or anything like that. Just nice, clean (or what passes as clean in Nova :) ) good code <3

.unbind()?
.bind(gc.nogc());
// 6. Perform ? ValidateTemporalUnitValue(largestUnit, unitGroup, « auto »).
// 7. If largestUnit is unset, then
Copy link
Member

Choose a reason for hiding this comment

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

question: Should these steps be added?

gc.into_nogc(),
));
};
let Ok(new_target) = Function::try_from(new_target.unbind()) else {
Copy link
Member

Choose a reason for hiding this comment

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

note: Here you do not want to unbind new_target before calling try_from: if left bound then the Ok(new_target) would still be bound to the GC lifetime and would be safe. If you didn't scope the result into a variable shadowing this name, it'd be possible for someone to accidentally use new_target after free.

.map_err(|e| temporal_err_to_js_err(agent, e, gc.nogc()))
.unbind()?
.bind(gc.nogc());
create_temporal_duration(agent, duration.unbind(), Some(new_target.get(agent)), gc)
Copy link
Member

Choose a reason for hiding this comment

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

note: It shouldn't be necessary to unbind duration, as temporal_rs::Duration is just "plain old data" and does not carry the GC lifetime. In other words, types that are trivially_bindable! do not need unbind/bind at function call interfaces.

};
use temporal_rs::{TemporalError, error::ErrorKind};

pub fn temporal_err_to_js_err<'gc>(
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: pub(crate) should suffice.

@@ -0,0 +1,33 @@
use crate::{
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: Missing MPLv2 header.

}

// 4. If roundTo is a String, then
let round_to = if round_to.unbind().is_string() {
Copy link
Member

Choose a reason for hiding this comment

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

note: Same thing here.

Suggested change
let round_to = if round_to.unbind().is_string() {
let round_to = if round_to.is_string() {

round_to
} else {
// 5. Else, set roundTo to ? GetOptionsObject(roundTo).
get_options_object(agent, round_to.unbind(), gc.nogc())
Copy link
Member

Choose a reason for hiding this comment

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

Note: Unbinding round_to should also be unnecessary here; unbinding is only needed when passing references (that are properly bound) into calls that take a GcScope.

.unbind()?
.bind(gc.nogc());
// 19. Return ! CreateTemporalInstant(roundedNs).
Ok(create_temporal_instant(agent, rounded_ns, None, gc)?.into_value())
Copy link
Member

Choose a reason for hiding this comment

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

note: This apparently cannot fail (! CreateTemporalInstant), so you can safely unwrap this. Or alternatively just use CreateHeapData directly.

return Ok(RoundingIncrement::default());
}
// 3. Let integerIncrement be ? ToIntegerWithTruncation(value).
let integer_increment = to_integer_with_truncation(agent, value, gc.reborrow())
Copy link
Member

Choose a reason for hiding this comment

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

thought: I think the below test will always return the same result even if you do a direct as i64 conversion here: the f64 cannot be infinite or NaN, it can at most be a much-too-big integer which (I think) round to i64::MAX and i64::MIN. Those'll still be outside the valid increment range even if value isn't exactly the same as originally.

dates: Vec::with_capacity(1024),
// TODO: assign appropriate value for Temporal objects.
#[cfg(feature = "temporal")]
instants: Vec::with_capacity(1024),
Copy link
Member

Choose a reason for hiding this comment

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

note: Probably 0.

@aapoalas aapoalas marked this pull request as ready for review December 11, 2025 18:08
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.

3 participants