From 921e5ac8fe64328f90bc2e3691e1b4e72d7399e1 Mon Sep 17 00:00:00 2001 From: Zeno Leonardi Date: Tue, 14 Apr 2026 10:59:26 +0200 Subject: [PATCH] echmascript(temporal): PlainTime.prototype.since + until --- nova_vm/src/builtin_strings | 1 + .../ecmascript/builtins/temporal/options.rs | 31 ++- .../builtins/temporal/plain_time.rs | 257 +++++++++++++++++- .../builtins/temporal/plain_time/data.rs | 1 + .../plain_time/plain_time_prototype.rs | 80 +++++- 5 files changed, 363 insertions(+), 7 deletions(-) diff --git a/nova_vm/src/builtin_strings b/nova_vm/src/builtin_strings index d3e7277e5..c816a0972 100644 --- a/nova_vm/src/builtin_strings +++ b/nova_vm/src/builtin_strings @@ -332,6 +332,7 @@ Object of #[cfg(feature = "atomics")]ok #[cfg(feature = "atomics")]or +#[cfg(feature = "temporal")]overflow ownKeys padEnd padStart diff --git a/nova_vm/src/ecmascript/builtins/temporal/options.rs b/nova_vm/src/ecmascript/builtins/temporal/options.rs index 0cde9f718..af0f57947 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/options.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/options.rs @@ -4,7 +4,7 @@ use std::str::FromStr; -use temporal_rs::options::{RoundingIncrement, RoundingMode, Unit}; +use temporal_rs::options::{Overflow, RoundingIncrement, RoundingMode, Unit}; use crate::{ ecmascript::{ @@ -68,6 +68,35 @@ impl StringOptionType for Unit { } } +impl StringOptionType for Overflow { + fn from_string<'gc>( + agent: &mut Agent, + value: String, + gc: NoGcScope<'gc, '_>, + ) -> JsResult<'gc, Self> { + let value = value.as_str(agent).unwrap_or(""); + Self::from_str(value).map_err(|err| { + agent.throw_exception(ExceptionType::RangeError, format!("{err}"), gc.into_nogc()) + }) + } +} + +/// ### [13.6 GetTemporalOverflowOption (options )](https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption) +/// +/// The abstract operation GetTemporalOverflowOption takes argument options (an Object) and returns either a normal completion containing either constrain or reject, or a throw completion. It fetches and validates the "overflow" property of options, returning a default if absent. It performs the following steps when called: + +pub(crate) fn get_temporal_overflow_option<'gc>( + agent: &mut Agent, + options: Object<'gc>, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::options::Overflow> { + let value = get_option::(agent, options, BUILTIN_STRING_MEMORY.overflow.into(), gc)?; + // 1. Let stringValue be ? GetOption(options, "overflow", string, « "constrain", "reject" », "constrain"). + // 2. If stringValue is "constrain", return constrain. + // 3. Return reject. + Ok(value.unwrap_or(Overflow::Reject)) +} + /// ### [14.5.2.1 GetOptionsObject ( options )](https://tc39.es/proposal-temporal/#sec-getoptionsobject) /// /// The abstract operation GetOptionsObject takes argument options (an ECMAScript language value) diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs index 29ad116e7..ca5b75c09 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs @@ -9,13 +9,13 @@ mod plain_time_prototype; pub(crate) use data::*; pub(crate) use plain_time_constructor::*; pub(crate) use plain_time_prototype::*; +use temporal_rs::options::{Overflow, Unit, UnitGroup}; use crate::{ ecmascript::{ - Agent, ExceptionType, Function, InternalMethods, InternalSlots, JsResult, OrdinaryObject, - ProtoIntrinsics, Value, object_handle, ordinary_populate_from_constructor, + Agent, BUILTIN_STRING_MEMORY, DurationRecord, ExceptionType, Function, InternalMethods, InternalSlots, JsResult, Object, OrdinaryObject, ProtoIntrinsics, String, TemporalDuration, Value, get, get_difference_settings, get_options_object, get_temporal_overflow_option, object_handle, ordinary_populate_from_constructor, temporal_err_to_js_err, to_integer_with_truncation }, - engine::{Bindable, GcScope, NoGcScope}, + engine::{Bindable, GcScope, NoGcScope, Scopable}, heap::{ ArenaAccess, ArenaAccessMut, BaseIndex, CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, HeapSweepWeakReference, WorkQueues, arena_vec_access, @@ -133,3 +133,254 @@ pub(crate) fn create_temporal_plain_time<'gc>( .unwrap(), ) } + +/// ### [4.5.6 ToTemporalTime ( item [ , options ] )](https://tc39.es/proposal-temporal/#sec-temporal-totemporaltime) +/// +/// The abstract operation ToTemporalTime takes argument item (an ECMAScript language value) and optional argument +/// options (an ECMAScript language value) and returns either a normal completion containing a Temporal.PlainTime +/// or a throw Completion. Converts item to a new Temporal.PlainTime instance if possible, and throws otherwise. +pub(crate) fn to_temporal_time<'gc>( + agent: &mut Agent, + item: Value, + options: Option, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::PlainTime> { + let item = item.bind(gc.nogc()); + + // 1. If options is not present, set options to undefined. + let options = options.unwrap_or(Value::Undefined); + // 2. If item is an Object, then + // a. If item has an [[InitializedTemporalTime]] internal slot, then + if let Value::PlainTime(time) = item { + // i. Let resolvedOptions be ? GetOptionsObject(options). + // let resolved_options = get_options_object(agent, options, gc.nogc()).unbind()?; + // ii. Perform ? GetTemporalOverflowOption(resolvedOptions). + // get_temporal_overflow_option(agent, resolved_options, gc.reborrow()).unbind()?; + // iii. Return ! CreateTemporalTime(item.[[Time]]). + return Ok(*time.inner_plain_time(agent)); + } else if let Value::Object(item) = item { + // b. If item has an [[InitializedTemporalDateTime]] internal slot, then + // i. Let resolvedOptions be ? GetOptionsObject(options). + // ii. Perform ? GetTemporalOverflowOption(resolvedOptions). + // iii. Return ! CreateTemporalTime(item.[[ISODateTime]].[[Time]]). + + // c. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then + // i. Let isoDateTime be GetISODateTimeFor(item.[[TimeZone]], item.[[EpochNanoseconds]]). + // ii. Let resolvedOptions be ? GetOptionsObject(options). + // iii. Perform ? GetTemporalOverflowOption(resolvedOptions). + // iv. Return ! CreateTemporalTime(isoDateTime.[[Time]]). + + // d. Let result be ? ToTemporalTimeRecord(item). + let result = to_temporal_time_record(agent, item.unbind().into(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // e. Let resolvedOptions be ? GetOptionsObject(options). + let resolved_options = get_options_object(agent, options, gc.nogc()) + .unbind()?; + // f. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + let overflow = if let Some(resolved_options) = resolved_options { + get_temporal_overflow_option(agent, resolved_options, gc.reborrow()) + .map_err(|e| e.unbind())? + } else { + Overflow::Constrain + }; + // g. Set result to ? RegulateTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]], overflow). + return temporal_rs::PlainTime::from_partial(result, Some(overflow)) + .map_err(|err| temporal_err_to_js_err(agent, err, gc.into_nogc())); + } + + // 3. Else, + // a. If item is not a String, throw a TypeError exception. + let Ok(item) = String::try_from(item) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "item is not a String", + gc.into_nogc(), + )); + }; + + // b. Let parseResult be ? ParseISODateTime(item, « TemporalTimeString »). + // c. Assert: parseResult.[[Time]] is not start-of-day. + // d. Set result to parseResult.[[Time]]. + // e. NOTE: A successful parse using TemporalTimeString guarantees absence of ambiguity with respect to any ISO 8601 date-only, year-month, or month-day representation. + let result = match temporal_rs::PlainTime::from_utf8(item.as_bytes(agent)) { + Ok(v) => v, + Err(err) => return Err(temporal_err_to_js_err(agent, err, gc.into_nogc())), + }; + // f. Let resolvedOptions be ? GetOptionsObject(options). + let resolved_options = get_options_object(agent, options, gc.nogc()).unbind()?; + // g. Perform ? GetTemporalOverflowOption(resolvedOptions). + if let Some(resolved_options) = resolved_options { + get_temporal_overflow_option(agent, resolved_options, gc.reborrow()) + .map_err(|e| e.unbind())?; + } + + // 4. Return ! CreateTemporalTime(result). + Ok(result) +} +/// ### [4.5.12 ToTemporalTimeRecord ( temporalTimeLike [ , completeness ] )](https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimerecord) +/// The abstract operation ToTemporalTimeRecord takes argument temporalTimeLike (an Object) and optional argument completeness (either partial or complete) and returns either a normal +/// completion containing a TemporalTimeLike Record or a throw completion. It converts temporalTimeLike to a TemporalTimeLike Record by reading time component properties, with missing +/// components set to 0 if completeness is complete or to unset if partial. It throws a TypeError if temporalTimeLike has no recognized time component properties. It performs the following steps when called: +fn to_temporal_time_record<'gc>( + agent: &mut Agent, + item: Object, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::partial::PartialTime> { + let item = item.scope(agent, gc.nogc()); + // 1. If completeness is not present, set completeness to complete. + // 2. If completeness is complete, then + // a. Let result be a new TemporalTimeLike Record with each field set to 0. + // 3. Else, + // a. Let result be a new TemporalTimeLike Record with each field set to unset. + let mut result = temporal_rs::partial::PartialTime::new(); + // 4. Let any be false. + let mut any = false; + // 5. Let hour be ? Get(temporalTimeLike, "hour"). + let hour = get(agent, item.get(agent), BUILTIN_STRING_MEMORY.hour.to_property_key(), gc.reborrow()).unbind()?.bind(gc.nogc()); + // 6. If hour is not undefined, then + if !hour.is_undefined() { + // a. Set result.[[Hour]] to ? ToIntegerWithTruncation(hour). + result.hour = Some(u8::try_from(to_integer_with_truncation(agent, hour.unbind(), gc.reborrow()).unbind()?).unwrap_or(u8::MAX)); + // b. Set any to true. + any = true; + } + // 7. Let microsecond be ? Get(temporalTimeLike, "microsecond"). + let microsecond = get(agent, item.get(agent), BUILTIN_STRING_MEMORY.microsecond.to_property_key(), gc.reborrow()).unbind()?.bind(gc.nogc()); + // 8. If microsecond is not undefined, then + if !microsecond.is_undefined() { + // a. Set result.[[Microsecond]] to ? ToIntegerWithTruncation(microsecond). + result.microsecond = Some(u16::try_from(to_integer_with_truncation(agent, microsecond.unbind(), gc.reborrow()).unbind()?).unwrap_or(u16::MAX)); + // b. Set any to true. + any = true; + } + // 9. Let millisecond be ? Get(temporalTimeLike, "millisecond"). + let millisecond = get(agent, item.get(agent), BUILTIN_STRING_MEMORY.millisecond.to_property_key(), gc.reborrow()).unbind()?.bind(gc.nogc()); + // 10. If millisecond is not undefined, then + if !millisecond.is_undefined() { + // a. Set result.[[Millisecond]] to ? ToIntegerWithTruncation(millisecond). + result.millisecond = Some(u16::try_from(to_integer_with_truncation(agent, millisecond.unbind(), gc.reborrow()).unbind()?).unwrap_or(u16::MAX)); + // b. Set any to true. + any = true; + } + // 11. Let minute be ? Get(temporalTimeLike, "minute"). + let minute = get(agent, item.get(agent), BUILTIN_STRING_MEMORY.minute.to_property_key(), gc.reborrow()).unbind()?.bind(gc.nogc()); + // 12. If minute is not undefined, then + if !minute.is_undefined() { + // a. Set result.[[Minute]] to ? ToIntegerWithTruncation(minute). + result.minute = Some(u8::try_from(to_integer_with_truncation(agent, minute.unbind(), gc.reborrow()).unbind()?).unwrap_or(u8::MAX)); + // b. Set any to true. + any = true; + } + // 13. Let nanosecond be ? Get(temporalTimeLike, "nanosecond"). + let nanosecond = get(agent, item.get(agent), BUILTIN_STRING_MEMORY.nanosecond.to_property_key(), gc.reborrow()).unbind()?.bind(gc.nogc()); + // 14. If nanosecond is not undefined, then + if !nanosecond.is_undefined() { + // a. Set result.[[Nanosecond]] to ? ToIntegerWithTruncation(nanosecond). + result.nanosecond = Some(u16::try_from(to_integer_with_truncation(agent, nanosecond.unbind(), gc.reborrow()).unbind()?).unwrap_or(u16::MAX)); + // b. Set any to true. + any = true; + } + // 15. Let second be ? Get(temporalTimeLike, "second"). + let second = get(agent, item.get(agent), BUILTIN_STRING_MEMORY.second.to_property_key(), gc.reborrow()).unbind()?.bind(gc.nogc()); + // 16. If second is not undefined, then + if !second.is_undefined() { + // a. Set result.[[Second]] to ? ToIntegerWithTruncation(second). + result.second = Some(u8::try_from(to_integer_with_truncation(agent, second.unbind(), gc.reborrow()).unbind()?).unwrap_or(u8::MAX)); + // b. Set any to true. + any = true; + } + // 17. If any is false, throw a TypeError exception. + if !any { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "TemporalTimeLike must have at least one time field", + gc.into_nogc(), + )); + } + // 18. Return result. + Ok(result) +} + +/// ### [4.5.17 DifferenceTemporalPlainTime ( operation, temporalTime, other, options )](https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplaintime) +/// The abstract operation DifferenceTemporalPlainTime takes arguments +/// operation (either since or until), temporalTime (a Temporal.PlainTime), +/// other (an ECMAScript language value), and options +/// (an ECMAScript language value) and returns either +/// a normal completion containing a Temporal.Duration or a +/// throw completion. It computes the difference between the +/// two times represented by temporalTime and other, optionally +/// rounds it, and returns it as a Temporal.Duration object. +pub(super) fn difference_temporal_plain_time<'gc, const IS_UNTIL: bool>( + agent: &mut Agent, + plain_time: TemporalPlainTime, + other: Value, + options: Value, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, TemporalDuration<'gc>> { + let plain_time = plain_time.scope(agent, gc.nogc()); + let other = other.bind(gc.nogc()); + let options = options.scope(agent, gc.nogc()); + // 1. Set other to ? ToTemporalTime(other). + let other = to_temporal_time(agent, other.unbind(), None, gc.reborrow()) + .unbind()?; + // 2. Let resolvedOptions be ? GetOptionsObject(options). + let resolved_option = get_options_object(agent, options.get(agent), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, + // time, « », nanosecond, hour). + // 4. Let timeDuration be + // DifferenceTime(temporalTime.[[Time]], other.[[Time]]). + // 5. Set timeDuration to ! RoundTimeDuration(timeDuration, + // settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]). + // 6. Let duration be + // CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration). + // 7. Let result be ! + // TemporalDurationFromInternal(duration, settings.[[LargestUnit]]). + // 8. If operation is since, set result to + // CreateNegatedTemporalDuration(result). + let duration = if IS_UNTIL { + const UNTIL: bool = true; + let settings = get_difference_settings::( + agent, + resolved_option.unbind(), + UnitGroup::Time, + &[], + Unit::Nanosecond, + Unit::Hour, + gc.reborrow(), + ) + .unbind()?; + temporal_rs::PlainTime::until( + plain_time.get(agent).inner_plain_time(agent), + &other, + settings, + ) + } else { + const SINCE: bool = false; + let settings = get_difference_settings::( + agent, + resolved_option.unbind(), + UnitGroup::Time, + &[], + Unit::Nanosecond, + Unit::Hour, + gc.reborrow(), + ) + .unbind()?; + temporal_rs::PlainTime::since( + plain_time.get(agent).inner_plain_time(agent), + &other, + settings, + ) + }; + let gc = gc.into_nogc(); + let duration = duration.map_err(|err| temporal_err_to_js_err(agent, err, gc))?; + + // 9. Return result. + Ok(agent.heap.create(DurationRecord { + object_index: None, + duration, + })) +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs index 9b0a7fd87..1e076b341 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs @@ -24,6 +24,7 @@ impl PlainTimeRecord<'_> { } trivially_bindable!(temporal_rs::PlainTime); +trivially_bindable!(temporal_rs::partial::PartialTime); bindable_handle!(PlainTimeRecord); impl HeapMarkAndSweep for PlainTimeRecord<'static> { diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_prototype.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_prototype.rs index 5b2aeb211..d7a2aee7a 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_prototype.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_prototype.rs @@ -6,9 +6,11 @@ use crate::{ ecmascript::{ Agent, ArgumentsList, BUILTIN_STRING_MEMORY, Behaviour, Builtin, BuiltinGetter, JsResult, PropertyKey, Realm, String, Value, builders::OrdinaryObjectBuilder, - builtins::temporal::plain_time::require_internal_slot_temporal_plain_time, + builtins::temporal::plain_time::{ + difference_temporal_plain_time, require_internal_slot_temporal_plain_time, + }, }, - engine::{GcScope, NoGcScope}, + engine::{Bindable, GcScope, NoGcScope}, heap::WellKnownSymbols, }; @@ -73,6 +75,20 @@ impl Builtin for TemporalPlainTimePrototypeGetNanosecond { } impl BuiltinGetter for TemporalPlainTimePrototypeGetNanosecond {} +struct TemporalPlainTimePrototypeUntil; +impl Builtin for TemporalPlainTimePrototypeUntil { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.until; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalPlainTimePrototype::until); +} + +struct TemporalPlainTimePrototypeSince; +impl Builtin for TemporalPlainTimePrototypeSince { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.since; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalPlainTimePrototype::since); +} + impl TemporalPlainTimePrototype { /// ### [4.3.4 get Temporal.PlainTime.prototype.minute](https://tc39.es/proposal-temporal/#sec-get-temporal.plaintime.prototype.minute) pub(crate) fn get_minute<'gc>( @@ -169,6 +185,62 @@ impl TemporalPlainTimePrototype { Ok(value.into()) } + /// ### [4.3.12 Temporal.PlainTime.prototype.until ( other [ , options ] )](https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.until) + fn until<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let other = args.get(0).bind(gc.nogc()); + let options = args.get(1).bind(gc.nogc()); + // 1. Let plainTime be the this value. + let plain_time = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(plainTime, [[InitializedTemporalTime]]). + let plain_time = + require_internal_slot_temporal_plain_time(agent, plain_time.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Return ? DifferenceTemporalPlainTime(until, plainTime, other, options). + const UNTIL: bool = true; + difference_temporal_plain_time::( + agent, + plain_time.unbind(), + other.unbind(), + options.unbind(), + gc, + ) + .map(Value::from) + } + + /// ### [4.3.13 Temporal.PlainTime.prototype.since ( other [ , options ] )](https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.since) + fn since<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let other = args.get(0).bind(gc.nogc()); + let options = args.get(1).bind(gc.nogc()); + // 1. Let plainTime be the this value. + let plain_time = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(plainTime, [[InitializedTemporalTime]]). + let plain_time = + require_internal_slot_temporal_plain_time(agent, plain_time.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Return ? DifferenceTemporalPlainTime(since, instant, other, options). + const SINCE: bool = false; + difference_temporal_plain_time::( + agent, + plain_time.unbind(), + other.unbind(), + options.unbind(), + gc, + ) + .map(Value::from) + } + pub(crate) fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _: NoGcScope) { let intrinsics = agent.get_realm_record_by_id(realm).intrinsics(); let this = intrinsics.temporal_plain_time_prototype(); @@ -176,7 +248,7 @@ impl TemporalPlainTimePrototype { let plain_time_constructor = intrinsics.temporal_plain_time(); OrdinaryObjectBuilder::new_intrinsic_object(agent, realm, this) - .with_property_capacity(8) + .with_property_capacity(10) .with_prototype(object_prototype) .with_constructor_property(plain_time_constructor) .with_builtin_function_getter_property::() @@ -185,6 +257,8 @@ impl TemporalPlainTimePrototype { .with_builtin_function_getter_property::() .with_builtin_function_getter_property::() .with_builtin_function_getter_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() .with_property(|builder| { builder .with_key(WellKnownSymbols::ToStringTag.into())