diff --git a/Cargo.toml b/Cargo.toml index 99cd30009..ef00e8745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ sonic-rs = "0.3.17" unicode-normalization = "0.1.24" usdt = { git = "https://github.com/aapoalas/usdt.git", branch = "nova-aarch64-branch" } wtf8 = "0.1" +temporal_rs = "0.1.0" [workspace.metadata.dylint] libraries = [{ path = "nova_lint" }] diff --git a/nova_vm/Cargo.toml b/nova_vm/Cargo.toml index b35c18c95..035c21995 100644 --- a/nova_vm/Cargo.toml +++ b/nova_vm/Cargo.toml @@ -39,6 +39,7 @@ sonic-rs = { workspace = true, optional = true } unicode-normalization = { workspace = true } usdt = { workspace = true } wtf8 = { workspace = true } +temporal_rs = { workspace = true, optional = true } [features] default = [ @@ -52,6 +53,7 @@ default = [ "regexp", "set", "annex-b", + "temporal", ] array-buffer = ["ecmascript_atomics"] atomics = ["array-buffer", "shared-array-buffer", "ecmascript_atomics", "ecmascript_futex"] @@ -63,6 +65,7 @@ shared-array-buffer = ["array-buffer", "ecmascript_atomics"] weak-refs = [] set = [] typescript = [] +temporal = ["temporal_rs"] # Enables features defined by [Annex B](https://tc39.es/ecma262/#sec-additional-ecmascript-features-for-web-browsers) annex-b = ["annex-b-string", "annex-b-global", "annex-b-date", "annex-b-regexp"] @@ -84,6 +87,7 @@ proposals = [ "proposal-math-clamp", "proposal-is-error", "proposal-atomics-microwait", + "proposal-temporal", ] # Enables the [Float16Array proposal](https://tc39.es/proposal-float16array/) proposal-float16array = ["array-buffer"] @@ -95,6 +99,8 @@ proposal-math-clamp = ["math"] proposal-is-error = [] # Enables the [Atomics.pause proposal](https://tc39.es/proposal-atomics-microwait/) proposal-atomics-microwait = ["atomics"] +# Enable the [Temporal proposal](https://tc39.es/proposal-temporal/) +proposal-temporal = ["temporal"] [build-dependencies] small_string = { path = "../small_string", version = "0.2.0" } diff --git a/nova_vm/src/builtin_strings b/nova_vm/src/builtin_strings index ae221de3d..6fb5c7a8b 100644 --- a/nova_vm/src/builtin_strings +++ b/nova_vm/src/builtin_strings @@ -28,7 +28,7 @@ __proto__ #[cfg(feature = "math")]abs #[cfg(feature = "math")]acos #[cfg(feature = "math")]acosh -#[cfg(any(feature = "atomics", feature = "set", feature = "weak-refs"))]add +#[cfg(any(feature = "atomics", feature = "set", feature = "weak-refs", feature = "temporal"))]add AggregateError all allSettled @@ -87,6 +87,7 @@ codePointAt concat configurable construct +compare constructor copyWithin #[cfg(feature = "math")]cos @@ -94,6 +95,7 @@ copyWithin create #[cfg(feature = "array-buffer")]DataView #[cfg(feature = "date")]Date +#[cfg(feature = "temporal")]days decodeURI decodeURIComponent default @@ -106,13 +108,17 @@ description #[cfg(feature = "array-buffer")]detached done #[cfg(feature = "regexp")]dotAll +#[cfg(feature="temporal")]Duration #[cfg(feature = "math")]E encodeURI encodeURIComponent endsWith entries enumerable +#[cfg(feature = "temporal")]epochMilliseconds +#[cfg(feature = "temporal")]epochNanoseconds EPSILON +#[cfg(feature = "temporal")]equals Error errors #[cfg(any(feature = "annex-b-string", feature = "regexp"))]escape @@ -133,6 +139,7 @@ find findIndex findLast findLastIndex +#[cfg(feature = "temporal")]fractionalSecondDigits #[cfg(feature = "annex-b-string")]fixed #[cfg(feature = "regexp")]flags flat @@ -147,6 +154,8 @@ for forEach freeze from +#[cfg(feature = "temporal")]fromEpochNanoseconds +#[cfg(feature = "temporal")]fromEpochMilliseconds fromCharCode fromCodePoint fromEntries @@ -183,6 +192,8 @@ get size #[cfg(feature = "array-buffer")]getBigUint64 #[cfg(feature = "date")]getDate #[cfg(feature = "date")]getDay +#[cfg(feature = "temporal")]get epochMilliseconds +#[cfg(feature = "temporal")]get epochNanoseconds #[cfg(feature = "proposal-float16array")]getFloat16 #[cfg(feature = "array-buffer")]getFloat32 #[cfg(feature = "array-buffer")]getFloat64 @@ -225,6 +236,7 @@ hasInstance hasOwn hasOwnProperty #[cfg(feature = "math")]hypot +#[cfg(feature = "temporal")]hours #[cfg(feature = "regexp")]ignoreCase #[cfg(feature = "math")]imul includes @@ -250,6 +262,8 @@ isSealed #[cfg(feature = "array-buffer")]isView isWellFormed #[cfg(feature = "annex-b-string")]italics +#[cfg(feature = "temporal")]Instant +#[cfg(feature = "temporal")]largestUnit Iterator iterator join @@ -281,6 +295,10 @@ MAX_SAFE_INTEGER MAX_VALUE #[cfg(feature = "array-buffer")]maxByteLength message +#[cfg(feature = "temporal")]microseconds +#[cfg(feature = "temporal")]milliseconds +#[cfg(feature = "temporal")]minutes +#[cfg(feature = "temporal")]months #[cfg(feature = "math")]min MIN_SAFE_INTEGER MIN_VALUE @@ -288,6 +306,7 @@ Module #[cfg(feature = "regexp")]multiline name NaN +#[cfg(feature = "temporal")]nanoseconds NEGATIVE_INFINITY next normalize @@ -310,6 +329,7 @@ parseFloat parseInt #[cfg(feature = "proposal-atomics-microwait")]pause #[cfg(feature = "math")]PI +#[cfg(feature = "temporal")]PlainTime pop POSITIVE_INFINITY #[cfg(feature = "math")]pow @@ -343,9 +363,12 @@ resolve return reverse revocable -#[cfg(feature = "math")]round +#[cfg(any(feature = "math", feature = "temporal"))]round +#[cfg(feature = "temporal")]roundingMode +#[cfg(feature = "temporal")]roundingIncrement seal #[cfg(feature = "regexp")]search +#[cfg(feature = "temporal")]seconds set #[cfg(feature = "set")]Set set [Symbol.toStringTag] @@ -381,10 +404,12 @@ setPrototypeOf shift #[cfg(feature = "math")]sign #[cfg(feature = "math")]sin +#[cfg(feature = "temporal")]since #[cfg(feature = "math")]sinh size slice #[cfg(feature = "annex-b-string")]small +#[cfg(feature = "temporal")]smallestUnit some sort #[cfg(feature = "regexp")]source @@ -407,6 +432,7 @@ String Iterator #[cfg(feature = "array-buffer")]subarray #[cfg(feature = "annex-b-string")]substr substring +#[cfg(feature = "temporal")]subtract #[cfg(feature = "proposal-math-sum")]sumPrecise #[cfg(feature = "annex-b-string")]sup symbol @@ -426,12 +452,16 @@ Symbol.toPrimitive Symbol.toStringTag Symbol.unscopables SyntaxError +#[cfg(feature = "temporal")]Temporal +#[cfg(feature = "temporal")]Temporal.Duration +#[cfg(feature = "temporal")]Temporal.Instant #[cfg(feature = "math")]tan #[cfg(feature = "math")]tanh #[cfg(feature = "regexp")]test then throw #[cfg(feature = "atomics")]timed-out +#[cfg(feature = "temporal")]timeZone toArray #[cfg(feature = "date")]toDateString toExponential @@ -454,6 +484,7 @@ toStringTag #[cfg(feature = "date")]toTimeString toUpperCase #[cfg(feature = "date")]toUTCString +#[cfg(feature = "temporal")]toZonedDateTimeISO toWellFormed #[cfg(feature = "array-buffer")]transfer #[cfg(feature = "array-buffer")]transferToFixedLength @@ -478,6 +509,7 @@ undefined unregister unscopables unshift +#[cfg(feature = "temporal")]until URIError #[cfg(feature = "date")]UTC value @@ -488,7 +520,9 @@ values #[cfg(feature = "weak-refs")]WeakMap #[cfg(feature = "weak-refs")]WeakRef #[cfg(feature = "weak-refs")]WeakSet +#[cfg(feature = "temporal")]weeks with withResolvers writable #[cfg(feature = "atomics")]xor +#[cfg(feature = "temporal")]years diff --git a/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs b/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs index b2a1571a5..7e502c5b3 100644 --- a/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs +++ b/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs @@ -1563,6 +1563,36 @@ pub(crate) fn try_to_index<'a>( js_result_into_try(validate_index(agent, integer, gc)) } +/// [14.5.1.1 ToIntegerIfIntegral ( argument )](https://tc39.es/proposal-temporal/#sec-tointegerifintegral) +/// The abstract operation ToIntegerIfIntegral takes argument argument +/// (an ECMAScript language value) and returns either a normal completion containing +/// 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>( + agent: &mut Agent, + argument: Value, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, Number<'gc>> { + let argument = argument.bind(gc.nogc()); + // 1. Let number be ? ToNumber(argument). + let number = to_number(agent, argument.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 2. If number is not an integral Number, throw a RangeError exception. + if !number.is_integer(agent) { + Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "Number must be integral", + gc.into_nogc(), + )) + } else { + // 3. Return ℝ(number). + // Ok(number.into_i64(agent)) // TODO: more performant + Ok(number.unbind()) + } +} + /// Helper function to check if a `char` is trimmable. /// /// Copied from Boa JS engine. Source https://github.com/boa-dev/boa/blob/183e763c32710e4e3ea83ba762cf815b7a89cd1f/core/string/src/lib.rs#L51 diff --git a/nova_vm/src/ecmascript/builtins.rs b/nova_vm/src/ecmascript/builtins.rs index 402495bc3..91bb8cd9e 100644 --- a/nova_vm/src/ecmascript/builtins.rs +++ b/nova_vm/src/ecmascript/builtins.rs @@ -66,6 +66,8 @@ pub(crate) mod set; #[cfg(feature = "shared-array-buffer")] pub(crate) mod shared_array_buffer; pub(crate) mod structured_data; +#[cfg(feature = "temporal")] +pub mod temporal; pub(crate) mod text_processing; #[cfg(feature = "array-buffer")] pub(crate) mod typed_array; diff --git a/nova_vm/src/ecmascript/builtins/ordinary.rs b/nova_vm/src/ecmascript/builtins/ordinary.rs index ab6da9c06..aec9db8e4 100644 --- a/nova_vm/src/ecmascript/builtins/ordinary.rs +++ b/nova_vm/src/ecmascript/builtins/ordinary.rs @@ -15,6 +15,11 @@ use caches::{CacheToPopulate, Caches, PropertyLookupCache, PropertyOffset}; #[cfg(feature = "shared-array-buffer")] use crate::ecmascript::builtins::data_view::data::SharedDataViewRecord; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::{ + duration::data::DurationHeapData, instant::data::InstantRecord, + plain_time::data::PlainTimeHeapData, +}; #[cfg(feature = "array-buffer")] use crate::ecmascript::types::try_get_result_into_value; use crate::{ @@ -1685,6 +1690,19 @@ pub(crate) fn ordinary_object_create_with_intrinsics<'a>( .heap .create(ErrorHeapData::new(ExceptionType::SyntaxError, None, None)) .into_object(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalInstant => { + agent.heap.create(InstantRecord::default()).into_object() + } + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalDuration => { + agent.heap.create(DurationHeapData::default()).into_object() + } + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalPlainTime => agent + .heap + .create(PlainTimeHeapData::default()) + .into_object(), ProtoIntrinsics::TypeError => agent .heap .create(ErrorHeapData::new(ExceptionType::TypeError, None, None)) @@ -2073,6 +2091,14 @@ fn get_intrinsic_constructor<'a>( ProtoIntrinsics::WeakRef => Some(intrinsics.weak_ref().into_function()), #[cfg(feature = "weak-refs")] ProtoIntrinsics::WeakSet => Some(intrinsics.weak_set().into_function()), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalInstant => Some(intrinsics.temporal_instant().into_function()), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalDuration => Some(intrinsics.temporal_duration().into_function()), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalPlainTime => { + Some(intrinsics.temporal_plain_time().into_function()) + } } } diff --git a/nova_vm/src/ecmascript/builtins/temporal.rs b/nova_vm/src/ecmascript/builtins/temporal.rs new file mode 100644 index 000000000..52b2e336c --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal.rs @@ -0,0 +1,260 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub mod duration; +pub mod error; +pub mod instant; +pub mod options; +pub mod plain_time; + +use temporal_rs::{ + options::{DifferenceSettings, RoundingIncrement, RoundingMode, Unit, UnitGroup}, + parsers::Precision, +}; + +use crate::{ + ecmascript::{ + abstract_operations::operations_on_objects::get, + builders::ordinary_object_builder::OrdinaryObjectBuilder, + builtins::temporal::{ + instant::instant_prototype::get_temporal_unit_valued_option, + options::{get_rounding_increment_option, get_rounding_mode_option}, + }, + execution::{Agent, JsResult, Realm, agent::ExceptionType}, + types::{BUILTIN_STRING_MEMORY, IntoValue, Object, Value}, + }, + engine::{ + context::{Bindable, GcScope, NoGcScope, trivially_bindable}, + rootable::Scopable, + }, + heap::WellKnownSymbolIndexes, +}; + +pub(crate) struct Temporal; + +impl Temporal { + pub fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _: NoGcScope) { + let intrinsics = agent.get_realm_record_by_id(realm).intrinsics(); + let object_prototype = intrinsics.object_prototype(); + let this = intrinsics.temporal(); + + let instant_constructor = intrinsics.temporal_instant(); + let duration_constructor = intrinsics.temporal_duration(); + let plain_time_constructor = intrinsics.temporal_plain_time(); + + OrdinaryObjectBuilder::new_intrinsic_object(agent, realm, this) + .with_property_capacity(4) + .with_prototype(object_prototype) + // 1.2.1 Temporal.Instant ( . . . ) + .with_property(|builder| { + builder + .with_key(BUILTIN_STRING_MEMORY.Instant.into()) + .with_value(instant_constructor.into_value()) + .with_enumerable(false) + .with_configurable(true) + .build() + }) + // 1.2.2 Temporal.PlainDateTime ( . . . ) + // 1.2.3 Temporal.PlainDate ( . . . ) + // 1.2.4 Temporal.PlainTime ( . . . ) + .with_property(|builder| { + builder + .with_key(BUILTIN_STRING_MEMORY.PlainTime.into()) + .with_value(plain_time_constructor.into_value()) + .with_enumerable(false) + .with_configurable(true) + .build() + }) + // 1.2.5 Temporal.PlainYearMonth ( . . . ) + // 1.2.6 Temporal.PlainMonthDay ( . . . ) + // 1.2.7 Temporal.Duration ( . . . ) + .with_property(|builder| { + builder + .with_key(BUILTIN_STRING_MEMORY.Duration.into()) + .with_value(duration_constructor.into_value()) + .with_enumerable(false) + .with_configurable(true) + .build() + }) + // 1.2.8 Temporal.ZonedDateTime ( . . . ) + // 1.3.1 Temporal.Now + .with_property(|builder| { + builder + .with_key(WellKnownSymbolIndexes::ToStringTag.into()) + .with_value_readonly(BUILTIN_STRING_MEMORY.Temporal.into()) + .with_enumerable(false) + .with_configurable(true) + .build() + }) + .build(); + } +} + +trivially_bindable!(DifferenceSettings); +trivially_bindable!(UnitGroup); +trivially_bindable!(Unit); +trivially_bindable!(RoundingMode); +trivially_bindable!(RoundingIncrement); +trivially_bindable!(Precision); + +/// [13.15 GetTemporalFractionalSecondDigitsOption ( options )](https://tc39.es/proposal-temporal/#sec-temporal-gettemporalfractionalseconddigitsoption) +/// The abstract operation GetTemporalFractionalSecondDigitsOption takes argument +/// options (an Object) and returns either a normal completion containing +/// either auto or an integer in the inclusive interval from 0 to 9, +/// or a throw completion. It fetches and validates the "fractionalSecondDigits" +/// property from options, returning a default if absent. +pub(crate) fn get_temporal_fractional_second_digits_option<'gc>( + agent: &mut Agent, + options: Object<'gc>, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::parsers::Precision> { + let options = options.bind(gc.nogc()); + // 1. Let digitsValue be ? Get(options, "fractionalSecondDigits"). + let mut digits_value = get( + agent, + options.unbind(), + BUILTIN_STRING_MEMORY + .fractionalSecondDigits + .to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 2. If digitsValue is undefined, return auto. + if digits_value.is_undefined() { + return Ok(temporal_rs::parsers::Precision::Auto); + } + if let Value::Integer(digits_value) = digits_value + && (0..=9).contains(&digits_value.into_i64()) + { + return Ok(temporal_rs::parsers::Precision::Digit( + digits_value.into_i64() as u8, + )); + } + // 3. If digitsValue is not a Number, then + if !digits_value.is_number() { + let scoped_digits_value = digits_value.scope(agent, gc.nogc()); + // a. If ? ToString(digitsValue) is not "auto", throw a RangeError exception. + if digits_value + .unbind() + .to_string(agent, gc.reborrow()) + .unbind()? + .as_bytes(agent) + != b"auto" + { + // b. Return auto. + return Ok(temporal_rs::parsers::Precision::Auto); + } + // SAFETY: not shared. + digits_value = unsafe { scoped_digits_value.take(agent) }.bind(gc.nogc()); + } + // 4. If digitsValue is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. + if digits_value.is_nan(agent) + || digits_value.is_pos_infinity(agent) + || digits_value.is_neg_infinity(agent) + { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "fractionalSecondDigits must be a finite number or \"auto\"", + gc.into_nogc(), + )); + } + // 5. Let digitCount be floor(ℝ(digitsValue)). + let digit_count = digits_value + .unbind() + .to_number(agent, gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + let digit_count = digit_count.into_f64(agent).floor(); + // 6. If digitCount < 0 or digitCount > 9, throw a RangeError exception. + if digit_count < 0.0 || digit_count > 9.0 { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "fractionalSecondDigits must be between 0 and 9", + gc.into_nogc(), + )); + } + // 7. Return digitCount. + Ok(temporal_rs::parsers::Precision::Digit(digit_count as u8)) +} + +/// [13.42 GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )](https://tc39.es/proposal-temporal/#sec-temporal-getdifferencesettings) +/// The abstract operation GetDifferenceSettings takes arguments operation (since or until), +/// options (an Object), unitGroup (date, time, or datetime), disallowedUnits (a List of Temporal units), +/// fallbackSmallestUnit (a Temporal unit), and smallestLargestDefaultUnit (a Temporal unit) and returns either +/// a normal completion containing a Record with fields [[SmallestUnit]] (a Temporal unit), +/// [[LargestUnit]] (a Temporal unit), [[RoundingMode]] (a rounding mode), +/// and [[RoundingIncrement]] (an integer in the inclusive interval from 1 to 10**9), +/// 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) + _unit_group: UnitGroup, // unitGroup (date, time, or datetime) + _disallowed_units: Vec, // disallowedUnits (todo:a List of Temporal units) + _fallback_smallest_unit: Unit, // fallbackSmallestUnit (a Temporal unit) + _smallest_largest_default_unit: Unit, // smallestLargestDefaultUnit (a Temporal unit) + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, DifferenceSettings> { + let _unit_group = _unit_group.bind(gc.nogc()); + let _disallowed_units = _disallowed_units.bind(gc.nogc()); + let _fallback_smallest_unit = _fallback_smallest_unit.bind(gc.nogc()); + let _smallest_largest_default_unit = _smallest_largest_default_unit.bind(gc.nogc()); + + let options = options.scope(agent, gc.nogc()); + // 1. NOTE: The following steps read options and perform independent validation in alphabetical order. + // 2. Let largestUnit be ? GetTemporalUnitValuedOption(options, "largestUnit", unset). + let largest_unit = get_temporal_unit_valued_option( + agent, + options.get(agent), + BUILTIN_STRING_MEMORY.largestUnit.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 3. Let roundingIncrement be ? GetRoundingIncrementOption(options). + let rounding_increment = + get_rounding_increment_option(agent, options.get(agent), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 4. Let roundingMode be ? GetRoundingModeOption(options, trunc). + let rounding_mode = get_rounding_mode_option( + agent, + options.get(agent), + RoundingMode::Trunc, + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 5. Let smallestUnit be ? GetTemporalUnitValuedOption(options, "smallestUnit", unset). + let smallest_unit = get_temporal_unit_valued_option( + agent, + options.get(agent), + BUILTIN_STRING_MEMORY.smallestUnit.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 6. Perform ? ValidateTemporalUnitValue(largestUnit, unitGroup, « auto »). + // 7. If largestUnit is unset, then + // a. Set largestUnit to auto. + // 8. If disallowedUnits contains largestUnit, throw a RangeError exception. + // 9. If operation is since, then + // a. Set roundingMode to NegateRoundingMode(roundingMode). + // 10. Perform ? ValidateTemporalUnitValue(smallestUnit, unitGroup). + // 11. If smallestUnit is unset, then + // a. Set smallestUnit to fallbackSmallestUnit. + // 12. If disallowedUnits contains smallestUnit, throw a RangeError exception. + // 13. Let defaultLargestUnit be LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit). + // 14. If largestUnit is auto, set largestUnit to defaultLargestUnit. + // 15. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception. + // 16. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit). + // 17. If maximum is not unset, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false). + // 18. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement, }. + let mut diff_settings = temporal_rs::options::DifferenceSettings::default(); + diff_settings.largest_unit = largest_unit; + diff_settings.smallest_unit = smallest_unit; + diff_settings.rounding_mode = Some(rounding_mode); + diff_settings.increment = Some(rounding_increment); + Ok(diff_settings) +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/duration.rs b/nova_vm/src/ecmascript/builtins/temporal/duration.rs new file mode 100644 index 000000000..aa18290d8 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/duration.rs @@ -0,0 +1,477 @@ +pub(crate) mod data; +pub mod duration_constructor; +pub mod duration_prototype; + +use crate::{ + ecmascript::{ + abstract_operations::{ + operations_on_objects::get, type_conversion::to_integer_if_integral, + }, + execution::{Agent, JsResult, ProtoIntrinsics, agent::ExceptionType}, + types::{ + BUILTIN_STRING_MEMORY, InternalMethods, InternalSlots, Object, OrdinaryObject, String, + Value, + }, + }, + engine::{ + context::{Bindable, GcScope, NoGcScope, bindable_handle}, + rootable::{HeapRootData, HeapRootRef, Rootable, Scopable}, + }, + heap::{ + CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, HeapSweepWeakReference, + WorkQueues, indexes::BaseIndex, + }, +}; +use core::ops::{Index, IndexMut}; + +use self::data::DurationHeapData; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct TemporalDuration<'a>(BaseIndex<'a, DurationHeapData<'static>>); + +impl TemporalDuration<'_> { + pub(crate) const fn _def() -> Self { + TemporalDuration(BaseIndex::from_u32_index(0)) + } + pub(crate) const fn get_index(self) -> usize { + self.0.into_index() + } + // pub(crate) fn inner_duration(self, agent: &Agent) -> temporal_rs::Duration { + // agent[self].duration + // } +} + +bindable_handle!(TemporalDuration); + +impl<'a> From> for Value<'a> { + fn from(value: TemporalDuration<'a>) -> Self { + Value::Duration(value) + } +} +impl<'a> From> for Object<'a> { + fn from(value: TemporalDuration<'a>) -> Self { + Object::Duration(value) + } +} +impl<'a> TryFrom> for TemporalDuration<'a> { + type Error = (); + + fn try_from(value: Value<'a>) -> Result { + match value { + Value::Duration(idx) => Ok(idx), + _ => Err(()), + } + } +} + +impl<'a> InternalSlots<'a> for TemporalDuration<'a> { + const DEFAULT_PROTOTYPE: ProtoIntrinsics = ProtoIntrinsics::TemporalDuration; + fn get_backing_object(self, agent: &Agent) -> Option> { + agent[self].object_index + } + fn set_backing_object(self, agent: &mut Agent, backing_object: OrdinaryObject<'static>) { + assert!(agent[self].object_index.replace(backing_object).is_none()); + } +} + +impl<'a> InternalMethods<'a> for TemporalDuration<'a> {} + +impl Index> for Agent { + type Output = DurationHeapData<'static>; + + fn index(&self, index: TemporalDuration<'_>) -> &Self::Output { + &self.heap.durations[index] + } +} + +impl IndexMut> for Agent { + fn index_mut(&mut self, index: TemporalDuration) -> &mut Self::Output { + &mut self.heap.durations[index] + } +} + +impl Index> for Vec> { + type Output = DurationHeapData<'static>; + + fn index(&self, index: TemporalDuration<'_>) -> &Self::Output { + self.get(index.get_index()) + .expect("heap access out of bounds") + } +} + +impl IndexMut> for Vec> { + fn index_mut(&mut self, index: TemporalDuration<'_>) -> &mut Self::Output { + self.get_mut(index.get_index()) + .expect("heap access out of bounds") + } +} + +impl Rootable for TemporalDuration<'_> { + type RootRepr = HeapRootRef; + + fn to_root_repr(value: Self) -> Result { + Err(HeapRootData::Duration(value.unbind())) + } + + fn from_root_repr(value: &Self::RootRepr) -> Result { + Err(*value) + } + + fn from_heap_ref(heap_ref: HeapRootRef) -> Self::RootRepr { + heap_ref + } + + fn from_heap_data(heap_data: HeapRootData) -> Option { + match heap_data { + HeapRootData::Duration(object) => Some(object), + _ => None, + } + } +} + +impl HeapMarkAndSweep for TemporalDuration<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + queues.durations.push(*self); + } + fn sweep_values(&mut self, compactions: &CompactionLists) { + compactions.durations.shift_index(&mut self.0); + } +} + +impl HeapSweepWeakReference for TemporalDuration<'static> { + fn sweep_weak_reference(self, compactions: &CompactionLists) -> Option { + compactions.durations.shift_weak_index(self.0).map(Self) + } +} + +impl<'a> CreateHeapData, TemporalDuration<'a>> for Heap { + fn create(&mut self, data: DurationHeapData<'a>) -> TemporalDuration<'a> { + self.durations.push(data.unbind()); + self.alloc_counter += core::mem::size_of::>(); + TemporalDuration(BaseIndex::last(&self.durations)) + } +} +/// 7.5.19 CreateTemporalDuration ( years, months, weeks, +/// days, hours, minutes, seconds, +/// milliseconds, microseconds, nanoseconds [ , newTarget ] ) +/// The abstract operation CreateTemporalDuration takes arguments +/// years (an integer), months (an integer), +/// weeks (an integer), days (an integer), +/// hours (an integer), minutes (an integer), +/// seconds (an integer), milliseconds (an integer), +/// microseconds (an integer), and nanoseconds (an integer) +/// and optional argument newTarget (a constructor) +/// and returns either a normal completion containing +/// a Temporal.Duration or a throw completion. +/// It creates a Temporal.Duration instance and fills +/// the internal slots with valid values. +/// It performs the following steps when called: +pub(crate) fn create_temporal_duration<'gc>(// years, + // months, + // weeks, + // days, + // hours, + // minutes, + // seconds, + // milliseconds, + // microseconds, + // nanoseconds: , + // new_target: Option, +) -> JsResult<'gc, TemporalDuration<'gc>> { + // 1. If IsValidDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) is false, throw a RangeError exception. + // 2. If newTarget is not present, set newTarget to %Temporal.Duration%. + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Duration.prototype%", « [[InitializedTemporalDuration]], [[Years]], [[Months]], [[Weeks]], [[Days]], [[Hours]], [[Minutes]], [[Seconds]], [[Milliseconds]], [[Microseconds]], [[Nanoseconds]] »). + // 4. Set object.[[Years]] to ℝ(𝔽(years)). + // 5. Set object.[[Months]] to ℝ(𝔽(months)). + // 6. Set object.[[Weeks]] to ℝ(𝔽(weeks)). + // 7. Set object.[[Days]] to ℝ(𝔽(days)). + // 8. Set object.[[Hours]] to ℝ(𝔽(hours)). + // 9. Set object.[[Minutes]] to ℝ(𝔽(minutes)). + // 10. Set object.[[Seconds]] to ℝ(𝔽(seconds)). + // 11. Set object.[[Milliseconds]] to ℝ(𝔽(milliseconds)). + // 12. Set object.[[Microseconds]] to ℝ(𝔽(microseconds)). + // 13. Set object.[[Nanoseconds]] to ℝ(𝔽(nanoseconds)). + // 14. Return object. + unimplemented!() +} + +/// Abstract Operations <---> + +/// [7.5.12 ToTemporalDuration ( item )](https://tc39.es/proposal-temporal/#sec-temporal-totemporalduration) +/// The abstract operation ToTemporalDuration takes argument item +/// (an ECMAScript language value) and returns either a normal completion containing a +/// Temporal.Duration or a throw completion. Converts item to a new Temporal.Duration +/// instance if possible and returns that, and throws otherwise. +/// It performs the following steps when called: +pub(crate) fn to_temporal_duration<'gc>( + agent: &mut Agent, + item: Value, + gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::Duration> { + let item = item.bind(gc.nogc()); + // 1. If item is an Object and item has an [[InitializedTemporalDuration]] internal slot, then + if let Ok(_obj) = require_internal_slot_temporal_duration(agent, item, gc.nogc()) { + unimplemented!(); + // a. Return ! CreateTemporalDuration(item.[[Years]], item.[[Months]], item.[[Weeks]], item.[[Days]], item.[[Hours]], item.[[Minutes]], item.[[Seconds]], item.[[Milliseconds]], item.[[Microseconds]], item.[[Nanoseconds]]). + } + // 2. If item is not an Object, then + if !item.is_object() { + let Ok(item) = String::try_from(item) else { + // a. If item is not a String, throw a TypeError exception. + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "item is not a string", + gc.into_nogc(), + )); + }; + // b. Return ? ParseTemporalDurationString(item). + let parsed = temporal_rs::Duration::from_utf8(item.as_bytes(agent)).unwrap(); + return Ok(parsed); + } + // 3. Let result be a new Partial Duration Record with each field set to 0. + // 4. Let partial be ? ToTemporalPartialDurationRecord(item). + let partial = + to_temporal_partial_duration_record(agent, Object::try_from(item).unwrap().unbind(), gc); + // 5. If partial.[[Years]] is not undefined, set result.[[Years]] to partial.[[Years]]. + // 6. If partial.[[Months]] is not undefined, set result.[[Months]] to partial.[[Months]]. + // 7. If partial.[[Weeks]] is not undefined, set result.[[Weeks]] to partial.[[Weeks]]. + // 8. If partial.[[Days]] is not undefined, set result.[[Days]] to partial.[[Days]]. + // 9. If partial.[[Hours]] is not undefined, set result.[[Hours]] to partial.[[Hours]]. + // 10. If partial.[[Minutes]] is not undefined, set result.[[Minutes]] to partial.[[Minutes]]. + // 11. If partial.[[Seconds]] is not undefined, set result.[[Seconds]] to partial.[[Seconds]]. + // 12. If partial.[[Milliseconds]] is not undefined, set result.[[Milliseconds]] to partial.[[Milliseconds]]. + // 13. If partial.[[Microseconds]] is not undefined, set result.[[Microseconds]] to partial.[[Microseconds]]. + // 14. If partial.[[Nanoseconds]] is not undefined, set result.[[Nanoseconds]] to partial.[[Nanoseconds]]. + // 15. Return ? CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). + Ok(temporal_rs::Duration::from_partial_duration(partial.unwrap()).unwrap()) +} + +/// [7.5.18 ToTemporalPartialDurationRecord ( temporalDurationLike )](https://tc39.es/proposal-temporal/#sec-temporal-totemporalpartialdurationrecord) +/// The abstract operation ToTemporalPartialDurationRecord takes argument temporalDurationLike +/// (an ECMAScript language value) and returns either a normal completion containing a +/// partial Duration Record or a throw completion. The returned Record has its fields +/// set according to the properties of temporalDurationLike. +pub(crate) fn to_temporal_partial_duration_record<'gc>( + agent: &mut Agent, + temporal_duration_like: Object, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::partial::PartialDuration> { + let temporal_duration_like = temporal_duration_like.scope(agent, gc.nogc()); + // 1. If temporalDurationLike is not an Object, then + // a. Throw a TypeError exception. + // 2. Let result be a new partial Duration Record with each field set to undefined. + let mut result = temporal_rs::partial::PartialDuration::empty(); + // 3. NOTE: The following steps read properties and perform independent validation in alphabetical order. + // 4. Let days be ? Get(temporalDurationLike, "days"). + let days = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.days.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 5. If days is not undefined, set result.[[Days]] to ? ToIntegerIfIntegral(days). + if !days.is_undefined() { + let days = to_integer_if_integral(agent, days.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.days = Some(days.into_i64(agent)) + } + // 6. Let hours be ? Get(temporalDurationLike, "hours"). + let hours = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.hours.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 7. If hours is not undefined, set result.[[Hours]] to ? ToIntegerIfIntegral(hours). + if !hours.is_undefined() { + let hours = to_integer_if_integral(agent, hours.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.hours = Some(hours.into_i64(agent)) + } + // 8. Let microseconds be ? Get(temporalDurationLike, "microseconds"). + let microseconds = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.microseconds.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 9. If microseconds is not undefined, set result.[[Microseconds]] to ? ToIntegerIfIntegral(microseconds). + if !microseconds.is_undefined() { + let microseconds = to_integer_if_integral(agent, microseconds.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.microseconds = Some(microseconds.into_i64(agent) as i128); + } + // 10. Let milliseconds be ? Get(temporalDurationLike, "milliseconds"). + let milliseconds = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.milliseconds.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 11. If milliseconds is not undefined, set result.[[Milliseconds]] to ? ToIntegerIfIntegral(milliseconds). + if !milliseconds.is_undefined() { + let milliseconds = to_integer_if_integral(agent, milliseconds.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.milliseconds = Some(milliseconds.into_i64(agent)) + } + // 12. Let minutes be ? Get(temporalDurationLike, "minutes"). + let minutes = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.minutes.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 13. If minutes is not undefined, set result.[[Minutes]] to ? ToIntegerIfIntegral(minutes). + if !minutes.is_undefined() { + let minutes = to_integer_if_integral(agent, minutes.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.minutes = Some(minutes.into_i64(agent)) + } + // 14. Let months be ? Get(temporalDurationLike, "months"). + let months = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.months.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 15. If months is not undefined, set result.[[Months]] to ? ToIntegerIfIntegral(months). + if !months.is_undefined() { + let months = to_integer_if_integral(agent, months.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.months = Some(months.into_i64(agent)) + } + // 16. Let nanoseconds be ? Get(temporalDurationLike, "nanoseconds"). + let nanoseconds = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.nanoseconds.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 17. If nanoseconds is not undefined, set result.[[Nanoseconds]] to ? ToIntegerIfIntegral(nanoseconds). + if !nanoseconds.is_undefined() { + let nanoseconds = to_integer_if_integral(agent, nanoseconds.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.nanoseconds = Some(nanoseconds.into_i64(agent) as i128); + } + // 18. Let seconds be ? Get(temporalDurationLike, "seconds"). + let seconds = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.seconds.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 19. If seconds is not undefined, set result.[[Seconds]] to ? ToIntegerIfIntegral(seconds). + if !seconds.is_undefined() { + let seconds = to_integer_if_integral(agent, seconds.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.seconds = Some(seconds.into_i64(agent)) + } + // 20. Let weeks be ? Get(temporalDurationLike, "weeks"). + let weeks = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.weeks.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 21. If weeks is not undefined, set result.[[Weeks]] to ? ToIntegerIfIntegral(weeks). + if !weeks.is_undefined() { + let weeks = to_integer_if_integral(agent, weeks.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.weeks = Some(weeks.into_i64(agent)) + } + // 22. Let years be ? Get(temporalDurationLike, "years"). + let years = get( + agent, + temporal_duration_like.get(agent), + BUILTIN_STRING_MEMORY.years.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 23. If years is not undefined, set result.[[Years]] to ? ToIntegerIfIntegral(years). + if !years.is_undefined() { + let years = to_integer_if_integral(agent, years.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + result.years = Some(years.into_i64(agent)) + } + // 24. If years is undefined, and months is undefined, and weeks is undefined, and days is undefined, and hours is undefined, and minutes is undefined, and seconds is undefined, and milliseconds is undefined, and microseconds is undefined, and nanoseconds is undefined, throw a TypeError exception. + if result.years.is_none() + && result.months.is_none() + && result.weeks.is_none() + && result.days.is_none() + && result.hours.is_none() + && result.minutes.is_none() + && result.seconds.is_none() + && result.milliseconds.is_none() + && result.microseconds.is_none() + && result.nanoseconds.is_none() + { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Duration must have at least one unit", + gc.into_nogc(), + )); + } + // 25. Return result. + Ok(result) +} + +/// [7.5.20 CreateNegatedTemporalDuration ( duration )] (https://tc39.es/proposal-temporal/#sec-temporal-createnegatedtemporalduration) +/// The abstract operation CreateNegatedTemporalDuration takes argument +/// duration (a Temporal.Duration) and returns a Temporal.Duration. +/// It returns a new Temporal.Duration instance that is the +/// negation of duration. +pub(crate) fn create_negated_temporal_duration<'gc>( + _agent: &mut Agent, + _item: temporal_rs::Duration, + mut _gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::Duration> { + // 1. Return ! CreateTemporalDuration(-duration.[[Years]], -duration.[[Months]], -duration.[[Weeks]], -duration.[[Days]], -duration.[[Hours]], -duration.[[Minutes]], -duration.[[Seconds]], -duration.[[Milliseconds]], -duration.[[Microseconds]], -duration.[[Nanoseconds]]). + unimplemented!() +} + +#[inline(always)] +fn require_internal_slot_temporal_duration<'a>( + _agent: &mut Agent, + _value: Value, + _gc: NoGcScope<'a, '_>, +) -> JsResult<'a, TemporalDuration<'a>> { + unimplemented!() + // TODO: + // match value { + // Value::Instant(instant) => Ok(instant.bind(gc)), + // _ => Err(agent.throw_exception_with_static_message( + // ExceptionType::TypeError, + // "Object is not a Temporal Instant", + // gc, + // )), + // } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/duration/data.rs b/nova_vm/src/ecmascript/builtins/temporal/duration/data.rs new file mode 100644 index 000000000..ef77cd728 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/duration/data.rs @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::engine::context::NoGcScope; +use crate::{ + ecmascript::types::OrdinaryObject, + engine::context::{bindable_handle, trivially_bindable}, + heap::{CompactionLists, HeapMarkAndSweep, WorkQueues}, +}; + +#[derive(Debug, Clone, Copy)] +pub struct DurationHeapData<'a> { + pub(crate) object_index: Option>, + pub(crate) duration: temporal_rs::Duration, +} + +impl DurationHeapData<'_> { + pub fn default() -> Self { + todo!() + } +} + +trivially_bindable!(temporal_rs::Duration); +bindable_handle!(DurationHeapData); + +impl HeapMarkAndSweep for DurationHeapData<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + let Self { + object_index, + duration: _, + } = self; + + object_index.mark_values(queues); + } + fn sweep_values(&mut self, compactions: &CompactionLists) { + let Self { + object_index, + duration: _, + } = self; + + object_index.sweep_values(compactions); + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/duration/duration_constructor.rs b/nova_vm/src/ecmascript/builtins/temporal/duration/duration_constructor.rs new file mode 100644 index 000000000..80c17eb07 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/duration/duration_constructor.rs @@ -0,0 +1,45 @@ +use crate::{ + ecmascript::{ + builders::builtin_function_builder::BuiltinFunctionBuilder, + builtins::{ArgumentsList, Behaviour, Builtin, BuiltinIntrinsicConstructor}, + execution::{Agent, JsResult, Realm}, + types::{BUILTIN_STRING_MEMORY, IntoObject, Object, String, Value}, + }, + engine::context::{GcScope, NoGcScope}, + heap::IntrinsicConstructorIndexes, +}; + +pub(crate) struct TemporalDurationConstructor; + +impl Builtin for TemporalDurationConstructor { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.Duration; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Constructor(TemporalDurationConstructor::constructor); +} +impl BuiltinIntrinsicConstructor for TemporalDurationConstructor { + const INDEX: IntrinsicConstructorIndexes = IntrinsicConstructorIndexes::TemporalDuration; +} + +impl TemporalDurationConstructor { + fn constructor<'gc>( + _agent: &mut Agent, + _: Value, + _args: ArgumentsList, + _new_target: Option, + mut _gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + unimplemented!() + } + + pub(crate) fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _gc: NoGcScope) { + let intrinsics = agent.get_realm_record_by_id(realm).intrinsics(); + let duration_prototype = intrinsics.temporal_duration_prototype(); + + BuiltinFunctionBuilder::new_intrinsic_constructor::( + agent, realm, + ) + .with_property_capacity(1) + .with_prototype_property(duration_prototype.into_object()) + .build(); + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/duration/duration_prototype.rs b/nova_vm/src/ecmascript/builtins/temporal/duration/duration_prototype.rs new file mode 100644 index 000000000..5d338a663 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/duration/duration_prototype.rs @@ -0,0 +1,33 @@ +use crate::{ + ecmascript::{ + builders::ordinary_object_builder::OrdinaryObjectBuilder, + execution::{Agent, Realm}, + types::BUILTIN_STRING_MEMORY, + }, + engine::context::NoGcScope, + heap::WellKnownSymbolIndexes, +}; + +pub(crate) struct TemporalDurationPrototype; +impl TemporalDurationPrototype { + pub fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _: NoGcScope) { + let intrinsics = agent.get_realm_record_by_id(realm).intrinsics(); + let this = intrinsics.temporal_duration_prototype(); + let object_prototype = intrinsics.object_prototype(); + let duration_constructor = intrinsics.temporal_duration(); + + OrdinaryObjectBuilder::new_intrinsic_object(agent, realm, this) + .with_property_capacity(2) + .with_prototype(object_prototype) + .with_constructor_property(duration_constructor) + .with_property(|builder| { + builder + .with_key(WellKnownSymbolIndexes::ToStringTag.into()) + .with_value_readonly(BUILTIN_STRING_MEMORY.Temporal_Duration.into()) + .with_enumerable(false) + .with_configurable(true) + .build() + }) + .build(); + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/error.rs b/nova_vm/src/ecmascript/builtins/temporal/error.rs new file mode 100644 index 000000000..3e81c43e9 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/error.rs @@ -0,0 +1,33 @@ +use crate::{ + ecmascript::execution::{ + Agent, + agent::{ExceptionType, JsError}, + }, + engine::context::NoGcScope, +}; +use temporal_rs::{TemporalError, error::ErrorKind}; + +pub fn temporal_err_to_js_err<'gc>( + agent: &mut Agent, + error: TemporalError, + gc: NoGcScope<'gc, '_>, +) -> JsError<'gc> { + let message = error.into_message(); + match error.kind() { + ErrorKind::Generic => { + agent.throw_exception_with_static_message(ExceptionType::Error, message, gc) + } + ErrorKind::Type => { + agent.throw_exception_with_static_message(ExceptionType::TypeError, message, gc) + } + ErrorKind::Range => { + agent.throw_exception_with_static_message(ExceptionType::RangeError, message, gc) + } + ErrorKind::Syntax => { + agent.throw_exception_with_static_message(ExceptionType::SyntaxError, message, gc) + } + ErrorKind::Assert => { + agent.throw_exception_with_static_message(ExceptionType::Error, message, gc) + } + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/instant.rs b/nova_vm/src/ecmascript/builtins/temporal/instant.rs new file mode 100644 index 000000000..1079ef0f2 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/instant.rs @@ -0,0 +1,412 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use core::ops::{Index, IndexMut}; + +pub(crate) mod data; +pub mod instant_constructor; +pub mod instant_prototype; + +use temporal_rs::options::{Unit, UnitGroup}; + +use crate::{ + ecmascript::{ + abstract_operations::type_conversion::{PreferredType, to_primitive_object}, + builtins::{ + ordinary::ordinary_create_from_constructor, + temporal::{ + duration::{TemporalDuration, data::DurationHeapData, to_temporal_duration}, + error::temporal_err_to_js_err, + get_difference_settings, + options::get_options_object, + }, + }, + execution::{ + JsResult, ProtoIntrinsics, + agent::{Agent, ExceptionType}, + }, + types::{ + Function, InternalMethods, InternalSlots, IntoFunction, IntoValue, Object, + OrdinaryObject, Primitive, String, Value, + }, + }, + engine::{ + context::{Bindable, GcScope, NoGcScope, bindable_handle}, + rootable::{HeapRootData, HeapRootRef, Rootable, Scopable}, + }, + heap::{ + CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, HeapSweepWeakReference, + WorkQueues, indexes::BaseIndex, + }, +}; + +use self::data::InstantRecord; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct TemporalInstant<'a>(BaseIndex<'a, InstantRecord<'static>>); + +impl TemporalInstant<'_> { + pub(crate) fn inner_instant(self, agent: &Agent) -> &temporal_rs::Instant { + &agent[self].instant + } + + //TODO + pub(crate) const fn _def() -> Self { + TemporalInstant(BaseIndex::from_u32_index(0)) + } + + pub(crate) const fn get_index(self) -> usize { + self.0.into_index() + } + + /// # Safety + /// + /// Should be only called once; reinitialising the value is not allowed. + unsafe fn set_epoch_nanoseconds( + self, + agent: &mut Agent, + epoch_nanoseconds: temporal_rs::Instant, + ) { + agent[self].instant = epoch_nanoseconds; + } +} + +bindable_handle!(TemporalInstant); + +impl<'a> From> for Value<'a> { + fn from(value: TemporalInstant<'a>) -> Self { + Value::Instant(value) + } +} +impl<'a> From> for Object<'a> { + fn from(value: TemporalInstant<'a>) -> Self { + Object::Instant(value) + } +} +impl<'a> TryFrom> for TemporalInstant<'a> { + type Error = (); + + fn try_from(value: Value<'a>) -> Result { + match value { + Value::Instant(idx) => Ok(idx), + _ => Err(()), + } + } +} +impl<'a> TryFrom> for TemporalInstant<'a> { + type Error = (); + fn try_from(object: Object<'a>) -> Result { + match object { + Object::Instant(idx) => Ok(idx), + _ => Err(()), + } + } +} + +impl<'a> InternalSlots<'a> for TemporalInstant<'a> { + const DEFAULT_PROTOTYPE: ProtoIntrinsics = ProtoIntrinsics::TemporalInstant; + fn get_backing_object(self, agent: &Agent) -> Option> { + agent[self].object_index + } + fn set_backing_object(self, agent: &mut Agent, backing_object: OrdinaryObject<'static>) { + assert!(agent[self].object_index.replace(backing_object).is_none()); + } +} + +impl<'a> InternalMethods<'a> for TemporalInstant<'a> {} + +// TODO: get rid of Index impls, replace with get/get_mut/get_direct/get_direct_mut functions +impl Index> for Agent { + type Output = InstantRecord<'static>; + + fn index(&self, index: TemporalInstant<'_>) -> &Self::Output { + &self.heap.instants[index] + } +} + +impl IndexMut> for Agent { + fn index_mut(&mut self, index: TemporalInstant) -> &mut Self::Output { + &mut self.heap.instants[index] + } +} + +impl Index> for Vec> { + type Output = InstantRecord<'static>; + + fn index(&self, index: TemporalInstant<'_>) -> &Self::Output { + self.get(index.get_index()) + .expect("heap access out of bounds") + } +} + +impl IndexMut> for Vec> { + fn index_mut(&mut self, index: TemporalInstant<'_>) -> &mut Self::Output { + self.get_mut(index.get_index()) + .expect("heap access out of bounds") + } +} + +impl Rootable for TemporalInstant<'_> { + type RootRepr = HeapRootRef; + + fn to_root_repr(value: Self) -> Result { + Err(HeapRootData::Instant(value.unbind())) + } + + fn from_root_repr(value: &Self::RootRepr) -> Result { + Err(*value) + } + + fn from_heap_ref(heap_ref: HeapRootRef) -> Self::RootRepr { + heap_ref + } + + fn from_heap_data(heap_data: HeapRootData) -> Option { + match heap_data { + HeapRootData::Instant(object) => Some(object), + _ => None, + } + } +} + +impl HeapMarkAndSweep for TemporalInstant<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + queues.instants.push(*self); + } + fn sweep_values(&mut self, compactions: &CompactionLists) { + compactions.instants.shift_index(&mut self.0); + } +} + +impl HeapSweepWeakReference for TemporalInstant<'static> { + fn sweep_weak_reference(self, compactions: &CompactionLists) -> Option { + compactions.instants.shift_weak_index(self.0).map(Self) + } +} + +impl<'a> CreateHeapData, TemporalInstant<'a>> for Heap { + fn create(&mut self, data: InstantRecord<'a>) -> TemporalInstant<'a> { + self.instants.push(data.unbind()); + self.alloc_counter += core::mem::size_of::>(); + TemporalInstant(BaseIndex::last(&self.instants)) + } +} + +/// 8.5.2 CreateTemporalInstant ( epochNanoseconds [ , newTarget ] ) +/// +/// The abstract operation CreateTemporalInstant takes argument +/// epochNanoseconds (a BigInt) and optional argument newTarget (a constructor) +/// and returns either a normal completion containing a Temporal.Instant or a +/// throw completion. It creates a Temporal.Instant instance and fills the +/// internal slots with valid values. +fn create_temporal_instant<'gc>( + agent: &mut Agent, + epoch_nanoseconds: temporal_rs::Instant, + new_target: Option, + gc: GcScope<'gc, '_>, +) -> JsResult<'gc, TemporalInstant<'gc>> { + // 1. Assert: IsValidEpochNanoseconds(epochNanoseconds) is true. + // 2. If newTarget is not present, set newTarget to %Temporal.Instant%. + let new_target = new_target.unwrap_or_else(|| { + agent + .current_realm_record() + .intrinsics() + .temporal_instant() + .into_function() + }); + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Instant.prototype%", « [[InitializedTemporalInstant]], [[EpochNanoseconds]] »). + let Object::Instant(object) = + ordinary_create_from_constructor(agent, new_target, ProtoIntrinsics::TemporalInstant, gc)? + else { + unreachable!() + }; + // 4. Set object.[[EpochNanoseconds]] to epochNanoseconds. + // SAFETY: initialising Instant. + unsafe { object.set_epoch_nanoseconds(agent, epoch_nanoseconds) }; + // 5. Return object. + Ok(object) +} + +/// ### [8.5.3 ToTemporalInstant ( item )](https://tc39.es/proposal-temporal/#sec-temporal-totemporalinstant) +/// +/// The abstract operation ToTemporalInstant takes argument item (an ECMAScript language value) and +/// returns either a normal completion containing a Temporal.Instant or a throw completion. +/// Converts item to a new Temporal.Instant instance if possible, and throws otherwise. +fn to_temporal_instant<'gc>( + agent: &mut Agent, + item: Value, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, temporal_rs::Instant> { + let item = item.bind(gc.nogc()); + // 1. If item is an Object, then + let item = if let Ok(item) = Object::try_from(item) { + // a. If item has an [[InitializedTemporalInstant]] or [[InitializedTemporalZonedDateTime]] + // internal slot, then + // TODO: TemporalZonedDateTime::try_from(item) + if let Ok(item) = TemporalInstant::try_from(item) { + // i. Return ! CreateTemporalInstant(item.[[EpochNanoseconds]]). + return Ok(agent[item].instant); + } + // b. NOTE: This use of ToPrimitive allows Instant-like objects to be converted. + // c. Set item to ? ToPrimitive(item, string). + to_primitive_object( + agent, + item.unbind(), + Some(PreferredType::String), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()) + } else { + Primitive::try_from(item).unwrap() + }; + // 2. 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(), + )); + }; + // 3. Let parsed be ? ParseISODateTime(item, « TemporalInstantString »). + // 4. Assert: Either parsed.[[TimeZone]].[[OffsetString]] is not empty or + // parsed.[[TimeZone]].[[Z]] is true, but not both. + // 5. If parsed.[[TimeZone]].[[Z]] is true, let offsetNanoseconds be 0; otherwise, let + // offsetNanoseconds be ! ParseDateTimeUTCOffset(parsed.[[TimeZone]].[[OffsetString]]). + // 6. If parsed.[[Time]] is start-of-day, let time be MidnightTimeRecord(); else let time be + // parsed.[[Time]]. + // 7. Let balanced be BalanceISODateTime(parsed.[[Year]], parsed.[[Month]], parsed.[[Day]], + // time.[[Hour]], time.[[Minute]], time.[[Second]], time.[[Millisecond]], + // time.[[Microsecond]], time.[[Nanosecond]] - offsetNanoseconds). + // 8. Perform ? CheckISODaysRange(balanced.[[ISODate]]). + // 9. Let epochNanoseconds be GetUTCEpochNanoseconds(balanced). + // 10. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + // 11. Return ! CreateTemporalInstant(epochNanoseconds). + let parsed = temporal_rs::Instant::from_utf8(item.as_bytes(agent)) + .map_err(|e| temporal_err_to_js_err(agent, e, gc.into_nogc()))?; + Ok(parsed) +} + +/// [8.5.10 AddDurationToInstant ( operation, instant, temporalDurationLike )](https://tc39.es/proposal-temporal/#sec-temporal-adddurationtoinstant) +/// The abstract operation AddDurationToInstant takes arguments operation +/// (add or subtract), instant (a Temporal.Instant), +/// and temporalDurationLike (an ECMAScript language value) +/// and returns either a normal completion containing a Temporal.Instant +/// or a throw completion. +/// It adds/subtracts temporalDurationLike to/from instant. +fn add_duration_to_instant<'gc, const IS_ADD: bool>( + agent: &mut Agent, + instant: TemporalInstant, + duration: Value, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, Value<'gc>> { + let duration = duration.bind(gc.nogc()); + let instant = instant.bind(gc.nogc()); + // 1. Let duration be ? ToTemporalDuration(temporalDurationLike). + let instant = instant.scope(agent, gc.nogc()); + let duration = to_temporal_duration(agent, duration.unbind(), gc.reborrow()); + // 2. If operation is subtract, set duration to CreateNegatedTemporalDuration(duration). + // 3. Let largestUnit be DefaultTemporalLargestUnit(duration). + // 4. If TemporalUnitCategory(largestUnit) is date, throw a RangeError exception. + // 5. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration). + // 6. Let ns be ? AddInstant(instant.[[EpochNanoseconds]], internalDuration.[[Time]]). + let ns_result = if IS_ADD { + temporal_rs::Instant::add(&agent[instant.get(agent)].instant, &duration.unwrap()).unwrap() + } else { + temporal_rs::Instant::subtract(&agent[instant.get(agent)].instant, &duration.unwrap()) + .unwrap() + }; + // 7. Return ! CreateTemporalInstant(ns). + let instant = create_temporal_instant(agent, ns_result, None, gc)?; + Ok(instant.into_value()) +} + +/// [8.5.9 DifferenceTemporalInstant ( operation, instant, other, options )](https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalinstant) +/// The abstract operation DifferenceTemporalInstant takes arguments +/// operation (since or until), instant (a Temporal.Instant), +/// 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 instant and other, optionally +/// rounds it, and returns it as a Temporal.Duration object. +fn difference_temporal_instant<'gc, const IS_UNTIL: bool>( + agent: &mut Agent, + instant: TemporalInstant, + other: Value, + options: Value, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, TemporalDuration<'gc>> { + let instant = instant.scope(agent, gc.nogc()); + let other = other.bind(gc.nogc()); + let options = options.scope(agent, gc.nogc()); + // 1. Set other to ? ToTemporalInstant(other). + let other = to_temporal_instant(agent, other.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 2. Let resolvedOptions be ? GetOptionsObject(options). + let resolved_options = get_options_object(agent, options.get(agent), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, time, « », nanosecond, second). + let _result; + if IS_UNTIL { + const UNTIL: bool = true; + let settings = get_difference_settings::( + agent, + resolved_options.unbind(), + UnitGroup::Time, + vec![], + Unit::Nanosecond, + Unit::Second, + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + _result = + temporal_rs::Instant::until(&instant.get(agent).inner_instant(agent), &other, settings); + } else { + const SINCE: bool = false; + let settings = get_difference_settings::( + agent, + resolved_options.unbind(), + UnitGroup::Time, + vec![], + Unit::Nanosecond, + Unit::Second, + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + _result = + temporal_rs::Instant::since(&instant.get(agent).inner_instant(agent), &other, settings); + } + // 4. Let internalDuration be DifferenceInstant(instant.[[EpochNanoseconds]], other.[[EpochNanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]). + // 5. Let result be ! TemporalDurationFromInternal(internalDuration, settings.[[LargestUnit]]). + // 6. If operation is since, set result to CreateNegatedTemporsalDuration(result). + // 7. Return result. + let result: temporal_rs::Duration = Default::default(); // TODO + // 7. Return result. + Ok(agent.heap.create(DurationHeapData { + object_index: None, + duration: result, + })) +} + +#[inline(always)] +fn require_internal_slot_temporal_instant<'a>( + agent: &mut Agent, + value: Value, + gc: NoGcScope<'a, '_>, +) -> JsResult<'a, TemporalInstant<'a>> { + match value { + Value::Instant(instant) => Ok(instant.bind(gc)), + _ => Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Object is not a Temporal Instant", + gc, + )), + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/instant/data.rs b/nova_vm/src/ecmascript/builtins/temporal/instant/data.rs new file mode 100644 index 000000000..1cf068d2e --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/instant/data.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::engine::context::NoGcScope; +use crate::{ + ecmascript::types::OrdinaryObject, + engine::context::{bindable_handle, trivially_bindable}, + heap::{CompactionLists, HeapMarkAndSweep, WorkQueues}, +}; + +#[derive(Debug, Clone, Copy)] +pub struct InstantRecord<'a> { + pub(crate) object_index: Option>, + pub(crate) instant: temporal_rs::Instant, +} + +impl InstantRecord<'_> { + pub fn default() -> Self { + Self { + object_index: None, + instant: temporal_rs::Instant::try_new(0).unwrap(), + } + } +} + +trivially_bindable!(temporal_rs::Instant); +bindable_handle!(InstantRecord); + +impl HeapMarkAndSweep for InstantRecord<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + let Self { + object_index, + instant: _, + } = self; + + object_index.mark_values(queues); + } + fn sweep_values(&mut self, compactions: &CompactionLists) { + let Self { + object_index, + instant: _, + } = self; + + object_index.sweep_values(compactions); + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/instant/instant_constructor.rs b/nova_vm/src/ecmascript/builtins/temporal/instant/instant_constructor.rs new file mode 100644 index 000000000..cfe8f385e --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/instant/instant_constructor.rs @@ -0,0 +1,241 @@ +use crate::{ + ecmascript::{ + abstract_operations::type_conversion::to_big_int, + builders::builtin_function_builder::BuiltinFunctionBuilder, + builtins::{ + ArgumentsList, Behaviour, Builtin, BuiltinIntrinsicConstructor, + temporal::instant::{ + create_temporal_instant, data::InstantRecord, to_temporal_instant, + }, + }, + execution::{Agent, JsResult, Realm, agent::ExceptionType}, + types::{ + BUILTIN_STRING_MEMORY, BigInt, Function, IntoObject, IntoValue, Object, String, Value, + }, + }, + engine::{ + context::{Bindable, GcScope, NoGcScope}, + rootable::Scopable, + }, + heap::{CreateHeapData, IntrinsicConstructorIndexes}, +}; + +/// Constructor function object for %Temporal.Instant%. +pub(crate) struct TemporalInstantConstructor; + +impl Builtin for TemporalInstantConstructor { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.Instant; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Constructor(TemporalInstantConstructor::constructor); +} +impl BuiltinIntrinsicConstructor for TemporalInstantConstructor { + const INDEX: IntrinsicConstructorIndexes = IntrinsicConstructorIndexes::TemporalInstant; +} + +struct TemporalInstantFrom; +impl Builtin for TemporalInstantFrom { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.from; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantConstructor::from); +} + +struct TemporalInstantFromEpochMilliseconds; +impl Builtin for TemporalInstantFromEpochMilliseconds { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.fromEpochMilliseconds; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = + Behaviour::Regular(TemporalInstantConstructor::from_epoch_milliseconds); +} + +struct TemporalInstantFromEpochNanoseconds; +impl Builtin for TemporalInstantFromEpochNanoseconds { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.fromEpochNanoseconds; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = + Behaviour::Regular(TemporalInstantConstructor::from_epoch_nanoseconds); +} + +struct TemporalInstantCompare; +impl Builtin for TemporalInstantCompare { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.compare; + const LENGTH: u8 = 2; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantConstructor::compare); +} + +impl TemporalInstantConstructor { + /// ### [8.1.1 Temporal.Instant ( epochNanoseconds )](https://tc39.es/proposal-temporal/#sec-temporal.instant) + fn constructor<'gc>( + agent: &mut Agent, + _: Value, + args: ArgumentsList, + new_target: Option, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let epoch_nanoseconds = args.get(0).bind(gc.nogc()); + let new_target = new_target.bind(gc.nogc()); + // 1. If NewTarget is undefined, throw a TypeError exception. + let Some(new_target) = new_target else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "calling a builtin Temporal.Instant constructor without new is forbidden", + gc.into_nogc(), + )); + }; + let Ok(mut new_target) = Function::try_from(new_target) else { + unreachable!() + }; + // 2. Let epochNanoseconds be ? ToBigInt(epochNanoseconds). + let epoch_nanoseconds = if let Ok(epoch_nanoseconds) = BigInt::try_from(epoch_nanoseconds) { + epoch_nanoseconds + } else { + let scoped_new_target = new_target.scope(agent, gc.nogc()); + let epoch_nanoseconds = to_big_int(agent, epoch_nanoseconds.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // SAFETY: not shared. + new_target = unsafe { scoped_new_target.take(agent) }.bind(gc.nogc()); + epoch_nanoseconds + }; + // 3. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + let Some(epoch_nanoseconds) = epoch_nanoseconds + .try_into_i128(agent) + .and_then(|nanoseconds| temporal_rs::Instant::try_new(nanoseconds).ok()) + else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "value out of range", + gc.into_nogc(), + )); + }; + // 4. Return ? CreateTemporalInstant(epochNanoseconds, NewTarget). + create_temporal_instant(agent, epoch_nanoseconds, Some(new_target.unbind()), gc) + .map(|instant| instant.into_value()) + } + + /// ### [8.2.2 Temporal.Instant.from ( item )](https://tc39.es/proposal-temporal/#sec-temporal.instant.from) + fn from<'gc>( + agent: &mut Agent, + _: Value, + args: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let item = args.get(0).bind(gc.nogc()); + // 1. Return ? ToTemporalInstant(item). + let instant = to_temporal_instant(agent, item.unbind(), gc)?; + let instant = agent.heap.create(InstantRecord { + object_index: None, + instant, + }); + Ok(instant.into_value()) + } + + /// ### [8.2.3 Temporal.Instant.fromEpochMilliseconds ( epochMilliseconds )](https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochmilliseconds) + fn from_epoch_milliseconds<'gc>( + agent: &mut Agent, + _: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let epoch_ms = args.get(0).bind(gc.nogc()); + // 1. Set epochMilliseconds to ? ToNumber(epochMilliseconds). + let epoch_ms_number = epoch_ms + .unbind() + .to_number(agent, gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 2. Set epochMilliseconds to ? NumberToBigInt(epochMilliseconds). + if !epoch_ms_number.is_integer(agent) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "Can't convert number to BigInt because it isn't an integer", + gc.into_nogc(), + )); + } + // 3. Let epochNanoseconds be epochMilliseconds × ℤ(10**6). + // 4. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + let epoch_ns = + match temporal_rs::Instant::from_epoch_milliseconds(epoch_ms_number.into_i64(agent)) { + Ok(instant) => instant, + Err(_) => { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "epochMilliseconds value out of range", + gc.into_nogc(), + )); + } + }; + + // 5. Return ! CreateTemporalInstant(epochNanoseconds). + let instant = create_temporal_instant(agent, epoch_ns, None, gc)?; + let value = instant.into_value(); + Ok(value) + } + + /// ### [8.2.4 Temporal.Instant.fromEpochNanoseconds ( epochNanoseconds )] (https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochnanoseconds) + fn from_epoch_nanoseconds<'gc>( + agent: &mut Agent, + _: Value, + arguments: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let epoch_nanoseconds = arguments.get(0).bind(gc.nogc()); + // 1. Set epochNanoseconds to ? ToBigInt(epochNanoseconds). + let epoch_nanoseconds = if let Ok(epoch_nanoseconds) = BigInt::try_from(epoch_nanoseconds) { + epoch_nanoseconds + } else { + to_big_int(agent, epoch_nanoseconds.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()) + }; + // 2. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + let Some(epoch_nanoseconds) = epoch_nanoseconds + .try_into_i128(agent) + .and_then(|nanoseconds| temporal_rs::Instant::try_new(nanoseconds).ok()) + else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "epochNanoseconds", + gc.into_nogc(), + )); + }; + // 3. Return ! CreateTemporalInstant(epochNanoseconds). + let instant = create_temporal_instant(agent, epoch_nanoseconds, None, gc)?; + let value = instant.into_value(); + Ok(value) + } + + /// ### [8.2.5 Temporal.Instant.compare ( one, two )](https://tc39.es/proposal-temporal/#sec-temporal.instant.compare) + fn compare<'gc>( + agent: &mut Agent, + _: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let one = args.get(0).bind(gc.nogc()); + let two = args.get(1).bind(gc.nogc()); + let two = two.scope(agent, gc.nogc()); + // 1. Set one to ? ToTemporalInstant(one). + let one_instant = to_temporal_instant(agent, one.unbind(), gc.reborrow()).unbind()?; + // 2. Set two to ? ToTemporalInstant(two). + let two_value = two.get(agent).bind(gc.nogc()); + let two_instant = to_temporal_instant(agent, two_value.unbind(), gc.reborrow()).unbind()?; + // 3. Return 𝔽(CompareEpochNanoseconds(one.[[EpochNanoseconds]], two.[[EpochNanoseconds]])). + Ok((one_instant.cmp(&two_instant) as i8).into()) + } + + pub(crate) fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _gc: NoGcScope) { + let intrinsics = agent.get_realm_record_by_id(realm).intrinsics(); + let instant_prototype = intrinsics.temporal_instant_prototype(); + + BuiltinFunctionBuilder::new_intrinsic_constructor::( + agent, realm, + ) + .with_property_capacity(5) + .with_prototype_property(instant_prototype.into_object()) + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .build(); + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/instant/instant_prototype.rs b/nova_vm/src/ecmascript/builtins/temporal/instant/instant_prototype.rs new file mode 100644 index 000000000..9d0289e08 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/instant/instant_prototype.rs @@ -0,0 +1,654 @@ +use temporal_rs::{ + TimeZone, + options::{RoundingMode, RoundingOptions, ToStringRoundingOptions, Unit}, +}; + +use crate::{ + ecmascript::{ + abstract_operations::{ + operations_on_objects::{get, try_create_data_property_or_throw}, + type_conversion::to_number, + }, + builders::ordinary_object_builder::OrdinaryObjectBuilder, + builtins::{ + ArgumentsList, Behaviour, Builtin, BuiltinGetter, + ordinary::ordinary_object_create_with_intrinsics, + temporal::{ + error::temporal_err_to_js_err, + get_temporal_fractional_second_digits_option, + instant::{ + add_duration_to_instant, create_temporal_instant, difference_temporal_instant, + require_internal_slot_temporal_instant, to_temporal_instant, + }, + options::{ + get_option, get_options_object, get_rounding_increment_option, + get_rounding_mode_option, + }, + }, + }, + execution::{ + Agent, JsResult, Realm, + agent::{ExceptionType, unwrap_try}, + }, + types::{BUILTIN_STRING_MEMORY, BigInt, IntoValue, Object, PropertyKey, String, Value}, + }, + engine::{ + context::{Bindable, GcScope, NoGcScope}, + rootable::Scopable, + }, + heap::WellKnownSymbolIndexes, +}; + +pub(crate) struct TemporalInstantPrototype; + +struct TemporalInstantPrototypeGetEpochMilliseconds; +impl Builtin for TemporalInstantPrototypeGetEpochMilliseconds { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.get_epochMilliseconds; + const KEY: Option> = + Some(BUILTIN_STRING_MEMORY.epochMilliseconds.to_property_key()); + const LENGTH: u8 = 0; + const BEHAVIOUR: Behaviour = + Behaviour::Regular(TemporalInstantPrototype::get_epoch_milliseconds); +} +impl BuiltinGetter for TemporalInstantPrototypeGetEpochMilliseconds {} + +struct TemporalInstantPrototypeGetEpochNanoSeconds; +impl Builtin for TemporalInstantPrototypeGetEpochNanoSeconds { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.get_epochNanoseconds; + const KEY: Option> = + Some(BUILTIN_STRING_MEMORY.epochNanoseconds.to_property_key()); + const LENGTH: u8 = 0; + const BEHAVIOUR: Behaviour = + Behaviour::Regular(TemporalInstantPrototype::get_epoch_nanoseconds); +} +impl BuiltinGetter for TemporalInstantPrototypeGetEpochNanoSeconds {} + +struct TemporalInstantPrototypeAdd; +impl Builtin for TemporalInstantPrototypeAdd { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.add; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::add); +} + +struct TemporalInstantPrototypeSubtract; +impl Builtin for TemporalInstantPrototypeSubtract { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.subtract; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::subtract); +} + +struct TemporalInstantPrototypeUntil; +impl Builtin for TemporalInstantPrototypeUntil { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.until; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::until); +} + +struct TemporalInstantPrototypeSince; +impl Builtin for TemporalInstantPrototypeSince { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.since; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::since); +} + +struct TemporalInstantPrototypeRound; +impl Builtin for TemporalInstantPrototypeRound { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.round; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::round); +} + +struct TemporalInstantPrototypeEquals; +impl Builtin for TemporalInstantPrototypeEquals { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.equals; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::equals); +} + +struct TemporalInstantPrototypeToString; +impl Builtin for TemporalInstantPrototypeToString { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.toString; + const LENGTH: u8 = 0; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::to_string); +} + +struct TemporalInstantPrototypeToLocaleString; +impl Builtin for TemporalInstantPrototypeToLocaleString { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.toLocaleString; + const LENGTH: u8 = 0; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::to_locale_string); +} + +struct TemporalInstantPrototypeToJSON; +impl Builtin for TemporalInstantPrototypeToJSON { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.toJSON; + const LENGTH: u8 = 0; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::to_json); +} + +struct TemporalInstantPrototypeValueOf; +impl Builtin for TemporalInstantPrototypeValueOf { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.valueOf; + const LENGTH: u8 = 0; + const BEHAVIOUR: Behaviour = Behaviour::Regular(TemporalInstantPrototype::value_of); +} + +struct TemporalInstantPrototypeToZonedDateTimeISO; +impl Builtin for TemporalInstantPrototypeToZonedDateTimeISO { + const NAME: String<'static> = BUILTIN_STRING_MEMORY.toZonedDateTimeISO; + const LENGTH: u8 = 1; + const BEHAVIOUR: Behaviour = + Behaviour::Regular(TemporalInstantPrototype::to_zoned_date_time_iso); +} + +impl TemporalInstantPrototype { + /// ### [8.3.3 get Temporal.Instant.prototype.epochMilliseconds](https://tc39.es/proposal-temporal/#sec-get-temporal.instant.prototype.epochmilliseconds) + fn get_epoch_milliseconds<'gc>( + agent: &mut Agent, + this_value: Value, + _: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let gc = gc.into_nogc(); + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, this_value, gc)?; + // 3. Let ns be instant.[[EpochNanoseconds]]. + // 4. Let ms be floor(ℝ(ns) / 10**6). + // 5. Return 𝔽(ms). + let value = instant.inner_instant(agent).epoch_milliseconds(); + Ok(Value::from_i64(agent, value, gc)) + } + + /// ### [8.3.4 get Temporal.Instant.prototype.epochNanoseconds](https://tc39.es/proposal-temporal/#sec-get-temporal.instant.prototype.epochnanoseconds) + fn get_epoch_nanoseconds<'gc>( + agent: &mut Agent, + this_value: Value, + _: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let gc = gc.into_nogc(); + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, this_value, gc)?; + // 3. Return instant.[[EpochNanoseconds]]. + let value = instant.inner_instant(agent).epoch_nanoseconds().as_i128(); + Ok(BigInt::from_i128(agent, value, gc).into()) + } + + /// ### [8.3.5 Temporal.Instant.prototype.add ( temporalDurationLike )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.add) + fn add<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let duration = args.get(0).bind(gc.nogc()); + // 1. Let instant be the this value. + let instant = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, instant.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Return ? AddDurationToInstant(add, instant, temporalDurationLike). + const ADD: bool = true; + let result = add_duration_to_instant::(agent, instant.unbind(), duration.unbind(), gc) + .unbind()?; + Ok(result.into_value()) + } + + /// ### [8.3.6 Temporal.Instant.prototype.subtract ( temporalDurationLike )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.subtract) + fn subtract<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let duration = args.get(0).bind(gc.nogc()); + // 1. Let instant be the this value. + let instant = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, instant.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Return ? AddDurationToInstant(subtract, instant, temporalDurationLike). + const SUBTRACT: bool = false; + let result = + add_duration_to_instant::(agent, instant.unbind(), duration.unbind(), gc) + .unbind()?; + Ok(result.into_value()) + } + + /// ### [8.3.7 Temporal.Instant.prototype.until ( other [ , options ] )](https://tc39.es/proposal-temporal/#sec-temporal.instant.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 instant be the this value. + let instant = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, instant.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Return ? DifferenceTemporalInstant(until, instant, other, options). + const UNTIL: bool = true; + let result = difference_temporal_instant::( + agent, + instant.unbind(), + other.unbind(), + options.unbind(), + gc, + ) + .unbind()?; + Ok(result.into_value()) + } + + /// ### [8.3.8 Temporal.Instant.prototype.since ( other [ , options ] )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.until) + 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 instant be the this value. + let instant = this_value; + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, instant.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()); + // 3. Return ? DifferenceTemporalInstant(since, instant, other, options). + const SINCE: bool = false; + let result = difference_temporal_instant::( + agent, + instant.unbind(), + other.unbind(), + options.unbind(), + gc, + ) + .unbind()?; + Ok(result.into_value()) + } + + /// ### [8.3.9 Temporal.Instant.prototype.round ( roundTo )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.round) + fn round<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let this_value = this_value.bind(gc.nogc()); + let round_to = args.get(0).bind(gc.nogc()); + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, this_value, gc.nogc()) + .unbind()? + .scope(agent, gc.nogc()); + + // 3. If roundTo is undefined, then + if round_to.unbind().is_undefined() { + // a. Throw a TypeError exception. + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "roundTo cannot be undefined", + gc.into_nogc(), + )); + } + + // 4. If roundTo is a String, then + let round_to = if round_to.unbind().is_string() { + // a. Let paramString be roundTo. + let param_string = round_to; + // b. Set roundTo to OrdinaryObjectCreate(null). + let round_to = ordinary_object_create_with_intrinsics(agent, None, None, gc.nogc()); + // c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString). + unwrap_try(try_create_data_property_or_throw( + agent, + round_to, + BUILTIN_STRING_MEMORY.smallestUnit.into(), + param_string.into_value(), + None, + gc.nogc(), + )); + round_to + } else { + // 5. Else, set roundTo to ? GetOptionsObject(roundTo). + get_options_object(agent, round_to.unbind(), gc.nogc()) + .unbind()? + .bind(gc.nogc()) + }; + + let round_to = round_to.scope(agent, gc.nogc()); + + // 6. NOTE: The following steps read options and perform independent validation in + // alphabetical order (GetRoundingIncrementOption reads "roundingIncrement" and + // GetRoundingModeOption reads "roundingMode"). + let mut options = RoundingOptions::default(); + + // 7. Let roundingIncrement be ? GetRoundingIncrementOption(roundTo). + let rounding_increment = + get_rounding_increment_option(agent, round_to.get(agent), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + options.increment = Some(rounding_increment); + + // 8. Let roundingMode be ? GetRoundingModeOption(roundTo, half-expand). + let rounding_mode = get_rounding_mode_option( + agent, + round_to.get(agent), + RoundingMode::default(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + options.rounding_mode = Some(rounding_mode); + + // 9. Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", required). + let smallest_unit = get_temporal_unit_valued_option( + agent, + round_to.get(agent), + BUILTIN_STRING_MEMORY.smallestUnit.into(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + options.smallest_unit = smallest_unit; + + // 10. Perform ? ValidateTemporalUnitValue(smallestUnit, time). + // 11. If smallestUnit is hour, then + // a. Let maximum be HoursPerDay. + // 12. Else if smallestUnit is minute, then + // a. Let maximum be MinutesPerHour × HoursPerDay. + // 13. Else if smallestUnit is second, then + // a. Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay. + // 14. Else if smallestUnit is millisecond, then + // a. Let maximum be ℝ(msPerDay). + // 15. Else if smallestUnit is microsecond, then + // a. Let maximum be 10**3 × ℝ(msPerDay). + // 16. Else, + // a. Assert: smallestUnit is nanosecond. + // b. Let maximum be nsPerDay. + // 17. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true). + // 18. Let roundedNs be RoundTemporalInstant(instant.[[EpochNanoseconds]], roundingIncrement, smallestUnit, roundingMode). + let rounded_ns = instant + .get(agent) + .inner_instant(agent) + .round(options) + .map_err(|e| temporal_err_to_js_err(agent, e, gc.nogc())) + .unbind()? + .bind(gc.nogc()); + // 19. Return ! CreateTemporalInstant(roundedNs). + Ok(create_temporal_instant(agent, rounded_ns, None, gc)?.into_value()) + } + + /// ### [8.3.10 Temporal.Instant.prototype.equals ( other )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.equals) + fn equals<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let other = args.get(0).bind(gc.nogc()); + let this_value = this_value.bind(gc.nogc()); + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, this_value.unbind(), gc.nogc()) + .unbind()? + .scope(agent, gc.nogc()); + // 3. Set other to ? ToTemporalInstant(other). + let other_instant = to_temporal_instant(agent, other.unbind(), gc.reborrow()).unbind()?; + // 4. If instant.[[EpochNanoseconds]] ≠ other.[[EpochNanoseconds]], return false. + let instant_val = instant.get(agent).bind(gc.nogc()); + if *instant_val.inner_instant(agent) != other_instant { + return Ok(Value::from(false)); + } + // 5. Return true. + Ok(Value::from(true)) + } + + /// ### [8.3.11 Temporal.Instant.prototype.toString ( [ options ] )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tostring) + fn to_string<'gc>( + agent: &mut Agent, + this_value: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + // 1. Let instant be the this value. + let options = args.get(0).bind(gc.nogc()); + let instant = this_value.bind(gc.nogc()); + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + let instant = require_internal_slot_temporal_instant(agent, instant.unbind(), gc.nogc()) + .unbind()? + .scope(agent, gc.nogc()); + // 3. Let resolvedOptions be ? GetOptionsObject(options). + let resolved_options = get_options_object(agent, options, gc.nogc()) + .unbind()? + .scope(agent, gc.nogc()); + // 4. NOTE: The following steps read options and perform independent + // validation in alphabetical order + // (GetTemporalFractionalSecondDigitsOption reads + // "fractionalSecondDigits" and GetRoundingModeOption reads + // "roundingMode"). + // 5. Let digits be ? GetTemporalFractionalSecondDigitsOption(resolvedOptions). + let digits = get_temporal_fractional_second_digits_option( + agent, + resolved_options.get(agent), + gc.reborrow(), + ) + .unbind()?; + // 6. Let roundingMode be ? GetRoundingModeOption(resolvedOptions, trunc). + let rounding_mode = get_rounding_mode_option( + agent, + resolved_options.get(agent), + RoundingMode::Trunc, + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 7. Let smallestUnit be ? GetTemporalUnitValuedOption(resolvedOptions, "smallestUnit", unset). + let smallest_unit = get_temporal_unit_valued_option( + agent, + resolved_options.get(agent), + BUILTIN_STRING_MEMORY.smallestUnit.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 8. Let timeZone be ? Get(resolvedOptions, "timeZone"). + let tz = get( + agent, + resolved_options.get(agent), + BUILTIN_STRING_MEMORY.timeZone.to_property_key(), + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + // 9. Perform ? ValidateTemporalUnitValue(smallestUnit, time). + if !smallest_unit.is_none_or(|su| su.is_time_unit()) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "smallestUnit is not a valid time unit", + gc.into_nogc(), + )); + } + // 10. If smallestUnit is hour, throw a RangeError exception. + if smallest_unit == Some(Unit::Hour) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "smallestUnit is hour", + gc.into_nogc(), + )); + } + // 11. If timeZone is not undefined, then + let time_zone = if !tz.is_undefined() { + // a. Set timeZone to ? ToTemporalTimeZoneIdentifier(timeZone). + Some(TimeZone::utc()) + } else { + None + }; + let instant = unsafe { instant.take(agent) }.bind(gc.nogc()); + // 12. Let precision be ToSecondsStringPrecisionRecord(smallestUnit, digits). + // 13. Let roundedNs be RoundTemporalInstant( + // instant.[[EpochNanoseconds]], + // precision.[[Increment]], + // precision.[[Unit]], + // roundingMode + // ). + // 14. Let roundedInstant be ! CreateTemporalInstant(roundedNs). + // 15. Return TemporalInstantToString(roundedInstant, timeZone, precision.[[Precision]]). + let options = ToStringRoundingOptions { + precision: digits, + smallest_unit, + rounding_mode: Some(rounding_mode), + }; + match instant + .inner_instant(agent) + .to_ixdtf_string(time_zone, options) + { + Ok(string) => Ok(Value::from_string(agent, string, gc.into_nogc())), + Err(err) => Err(temporal_err_to_js_err(agent, err, gc.into_nogc())), + } + } + + /// ### [8.3.12 Temporal.Instant.prototype.toLocaleString ( [ locales [ , options ] ] )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tolocalestring) + fn to_locale_string<'gc>( + _agent: &mut Agent, + _this_value: Value, + _args: ArgumentsList, + mut _gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + unimplemented!() + } + + /// ###[8.3.13 Temporal.Instant.prototype.toJSON ( )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tojson) + fn to_json<'gc>( + _agent: &mut Agent, + _this_value: Value, + _args: ArgumentsList, + mut _gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + unimplemented!() + } + + /// ###[8.3.14 Temporal.Instant.prototype.valueOf ( )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.valueof) + /// Note: + /// This method always throws, because in the absence of valueOf(), expressions with + /// arithmetic operators such as instant1 > instant2 would fall back to being equivalent + /// to instant1.toString() > instant2.toString(). Lexicographical comparison of + /// serialized strings might not seem obviously wrong, because the result would + /// sometimes be correct. Implementations are encouraged to phrase the error message to + /// point users to Temporal.Instant.compare(), Temporal.Instant.prototype.equals(), + /// and/or Temporal.Instant.prototype.toString(). + fn value_of<'gc>( + agent: &mut Agent, + _: Value, + _: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + // 1. Throw a TypeError exception. + Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`", + gc.into_nogc(), + )) + } + + // [8.3.15 Temporal.Instant.prototype.toZonedDateTimeISO ( timeZone )](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tozoneddatetimeiso) + fn to_zoned_date_time_iso<'gc>( + _agent: &mut Agent, + _this_value: Value, + _args: ArgumentsList, + mut _gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + unimplemented!() + } + + pub fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _: NoGcScope) { + let intrinsics = agent.get_realm_record_by_id(realm).intrinsics(); + let this = intrinsics.temporal_instant_prototype(); + let object_prototype = intrinsics.object_prototype(); + let instant_constructor = intrinsics.temporal_instant(); + + OrdinaryObjectBuilder::new_intrinsic_object(agent, realm, this) + .with_property_capacity(15) + .with_prototype(object_prototype) + .with_constructor_property(instant_constructor) + .with_builtin_function_getter_property::() + .with_builtin_function_getter_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_builtin_function_property::() + .with_property(|builder| { + builder + .with_key(WellKnownSymbolIndexes::ToStringTag.into()) + .with_value_readonly(BUILTIN_STRING_MEMORY.Temporal_Instant.into()) + .with_enumerable(false) + .with_configurable(true) + .build() + }) + .build(); + } +} + +/// ### [13.40 ToIntegerWithTruncation ( argument )] (https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation) +/// +/// The abstract operation ToIntegerWithTruncation takes argument argument (an ECMAScript language +/// value) and returns either a normal completion containing an integer or a throw completion. It +/// converts argument to an integer representing its Number value with fractional part truncated, or +/// throws a RangeError when that value is not finite. It performs the following steps when called: +pub(crate) fn to_integer_with_truncation<'gc>( + agent: &mut Agent, + argument: Value, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, f64> { + let argument = argument.bind(gc.nogc()); + // 1. Let number be ? ToNumber(argument). + let number = to_number(agent, argument.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + + // 2. If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception. + if number.is_nan(agent) || number.is_pos_infinity(agent) || number.is_neg_infinity(agent) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "Number cannot be NaN, positive infinity, or negative infinity", + gc.into_nogc(), + )); + } + + // 3. Return truncate(ℝ(number)). + Ok(number.into_f64(agent).trunc()) +} + +/// ### [13.17 GetTemporalUnitValuedOption ( options, key, default )] (https://tc39.es/proposal-temporal/#sec-temporal-gettemporalunitvaluedoption) +/// +/// The abstract operation GetTemporalUnitValuedOption takes arguments options (an Object), key (a +/// property key), and default (required or unset) and returns either a normal completion +/// containing either a Temporal unit, unset, or auto, or a throw completion. It attempts to read a +/// Temporal unit from the specified property of options. +pub(crate) fn get_temporal_unit_valued_option<'gc>( + agent: &mut Agent, + options: Object, + key: PropertyKey, + gc: GcScope<'gc, '_>, +) -> JsResult<'gc, Option> { + let options = options.bind(gc.nogc()); + let key = key.bind(gc.nogc()); + + let opt = get_option::(agent, options.unbind(), key.unbind(), gc)?; + + Ok(opt) +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/options.rs b/nova_vm/src/ecmascript/builtins/temporal/options.rs new file mode 100644 index 000000000..06bf109c1 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/options.rs @@ -0,0 +1,213 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::{num::NonZeroU32, str::FromStr}; + +use temporal_rs::options::{RoundingIncrement, RoundingMode, Unit}; + +use crate::{ + ecmascript::{ + abstract_operations::{operations_on_objects::get, type_conversion::to_string}, + builtins::{ + ordinary::ordinary_object_create_with_intrinsics, + temporal::instant::instant_prototype::to_integer_with_truncation, + }, + execution::{Agent, JsResult, agent::ExceptionType}, + types::{BUILTIN_STRING_MEMORY, Object, PropertyKey, Value}, + }, + engine::context::{Bindable, GcScope, NoGcScope}, +}; + +pub trait OptionType: Sized { + fn from_string(s: &str) -> Result; +} + +impl OptionType for RoundingMode { + fn from_string(s: &str) -> Result { + RoundingMode::from_str(s).map_err(|e| e.to_string()) + } +} + +impl OptionType for Unit { + fn from_string(s: &str) -> Result { + Unit::from_str(s).map_err(|e| e.to_string()) + } +} + +/// ### [14.5.2.1 GetOptionsObject ( options )](https://tc39.es/proposal-temporal/#sec-getoptionsobject) +/// +/// The abstract operation GetOptionsObject takes argument options (an ECMAScript language value) +/// and returns either a normal completion containing an Object or a throw completion. It returns +/// an Object suitable for use with GetOption, either options itself or a default empty Object. It +/// throws a TypeError if options is not undefined and not an Object. It performs the following +/// steps when called: +pub(crate) fn get_options_object<'gc>( + agent: &mut Agent, + options: Value, + gc: NoGcScope<'gc, '_>, +) -> JsResult<'gc, Object<'gc>> { + match options.unbind() { + // 1. If options is undefined, then + Value::Undefined => { + // a. Return OrdinaryObjectCreate(null). + Ok(ordinary_object_create_with_intrinsics( + agent, None, None, gc, + )) + } + // 2. If options is an Object, then + value if value.is_object() => { + // a. Return options. + // TODO: remove unwrap; Although safe because value is an object + let obj = Object::try_from(value).unwrap(); + Ok(obj) + } + // 3. Throw a TypeError exception. + _ => Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "options provided to GetOptionsObject is not an object", + gc, + )), + } +} + +/// ### [14.5.2.2 GetOption ( options, property, type, values, default )](https://tc39.es/proposal-temporal/#sec-getoption) +/// +/// The abstract operation GetOption takes arguments options (an Object), property (a property +/// key), type (boolean or string), values (empty or a List of ECMAScript language values), and +/// default (required or an ECMAScript language value) and returns either a normal completion +/// containing an ECMAScript language value or a throw completion. It extracts the value of the +/// specified property of options, converts it to the required type, checks whether it is allowed +/// by values if values is not empty, and substitutes default if the value is undefined. It +/// performs the following steps when called: +pub(crate) fn get_option<'gc, T>( + agent: &mut Agent, + options: Object, + property: PropertyKey, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, Option> +where + T: OptionType, +{ + let options = options.bind(gc.nogc()); + let property = property.bind(gc.nogc()); + // 1. Let value be ? Get(options, property). + let value = get(agent, options.unbind(), property.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + // 2. If value is undefined, then + if value.is_undefined() { + // a. If default is required, throw a RangeError exception. + // b. Return default. + return Ok(None); + } + + // 3. If type is boolean, then + // a. Set value to ToBoolean(value). + // 4. Else, + // a. Assert: type is string. + // b. Set value to ? ToString(value). + // 5. If values is not empty and values does not contain value, throw a RangeError exception. + + // TODO: Currently only works for temporal_rs::Unit, and temporal_rs::RoundingMode. + // + // Should be extended to work with + // 1. ecmascript::types::String + // 2. bool + // 3. Potentially other temporal_rs types. + + let js_str = to_string(agent, value.unbind(), gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + + // TODO: Fix this code.. None case is unreachable but code sucks rn.. + let rust_str = js_str.as_str(agent).unwrap(); + + let parsed = T::from_string(rust_str).map_err(|msg| { + agent.throw_exception_with_static_message( + ExceptionType::RangeError, + Box::leak(msg.into_boxed_str()), + gc.into_nogc(), + ) + })?; + + // 6. Return value. + Ok(Some(parsed)) +} + +/// ### [14.5.2.3 GetRoundingModeOption ( options, fallback)](https://tc39.es/proposal-temporal/#sec-temporal-getroundingmodeoption) +/// +// The abstract operation GetRoundingModeOption takes arguments options (an Object) and fallback (a +// rounding mode) and returns either a normal completion containing a rounding mode, or a throw +// completion. It fetches and validates the "roundingMode" property from options, returning +// fallback as a default if absent. It performs the following steps when called: +pub(crate) fn get_rounding_mode_option<'gc>( + agent: &mut Agent, + options: Object, + fallback: RoundingMode, + gc: GcScope<'gc, '_>, +) -> JsResult<'gc, RoundingMode> { + let options = options.bind(gc.nogc()); + let fallback = fallback.bind(gc.nogc()); + + // 1. Let allowedStrings be the List of Strings from the "String Identifier" column of Table 28. + // 2. Let stringFallback be the value from the "String Identifier" column of the row with fallback in its "Rounding Mode" column. + // 3. Let stringValue be ? GetOption(options, "roundingMode", string, allowedStrings, stringFallback). + // 4. Return the value from the "Rounding Mode" column of the row with stringValue in its "String Identifier" column. + match get_option::( + agent, + options.unbind(), + BUILTIN_STRING_MEMORY.roundingMode.into(), + gc, + )? { + Some(mode) => Ok(mode), + None => Ok(fallback), + } +} + +/// ### [14.5.2.4 GetRoundingIncrementOption ( options )](https://tc39.es/proposal-temporal/#sec-temporal-getroundingincrementoption) +/// +/// The abstract operation GetRoundingIncrementOption takes argument options (an Object) and returns +/// either a normal completion containing a positive integer in the inclusive interval from 1 to +/// 10**9, or a throw completion. It fetches and validates the "roundingIncrement" property from +/// options, returning a default if absent. It performs the following steps when called: +pub(crate) fn get_rounding_increment_option<'gc>( + agent: &mut Agent, + options: Object, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, RoundingIncrement> { + let options = options.bind(gc.nogc()); + // 1. Let value be ? Get(options, "roundingIncrement"). + let value = get( + agent, + options.unbind(), + BUILTIN_STRING_MEMORY.roundingIncrement.into(), + gc.reborrow(), + ) + .unbind()?; + // 2. If value is undefined, return 1𝔽. + if value.is_undefined() { + return Ok(RoundingIncrement::default()); + } + // 3. Let integerIncrement be ? ToIntegerWithTruncation(value). + let integer_increment = to_integer_with_truncation(agent, value, gc.reborrow()) + .unbind()? + .bind(gc.nogc()); + + // 4. If integerIncrement < 1 or integerIncrement > 10**9, throw a RangeError exception. + if !(1.0..=1_000_000_000.0).contains(&integer_increment) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "roundingIncrement must be between 1 and 10**9", + gc.into_nogc(), + )); + } + + // NOTE: `as u32` is safe here since we validated it’s in range. + let integer_increment_u32 = integer_increment as u32; + let increment = + NonZeroU32::new(integer_increment_u32).expect("integer_increment >= 1 ensures nonzero"); + + // 5. Return integerIncrement. + Ok(RoundingIncrement::new_unchecked(increment)) +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs new file mode 100644 index 000000000..f451d8e65 --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs @@ -0,0 +1,157 @@ +use std::ops::{Index, IndexMut}; + +use crate::{ + ecmascript::{ + builtins::temporal::plain_time::data::PlainTimeHeapData, + execution::{Agent, ProtoIntrinsics}, + types::{InternalMethods, InternalSlots, Object, OrdinaryObject, Value}, + }, + engine::{ + context::{Bindable, bindable_handle}, + rootable::{HeapRootData, HeapRootRef, Rootable}, + }, + heap::{ + CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, HeapSweepWeakReference, + WorkQueues, indexes::BaseIndex, + }, +}; + +pub(crate) mod data; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct TemporalPlainTime<'a>(BaseIndex<'a, PlainTimeHeapData<'static>>); + +impl TemporalPlainTime<'_> { + //TODO + pub(crate) const fn _def() -> Self { + TemporalPlainTime(BaseIndex::from_u32_index(0)) + } + + pub(crate) const fn get_index(self) -> usize { + self.0.into_index() + } +} + +bindable_handle!(TemporalPlainTime); + +impl<'a> From> for Value<'a> { + fn from(value: TemporalPlainTime<'a>) -> Self { + Value::PlainTime(value) + } +} +impl<'a> From> for Object<'a> { + fn from(value: TemporalPlainTime<'a>) -> Self { + Object::PlainTime(value) + } +} +impl<'a> TryFrom> for TemporalPlainTime<'a> { + type Error = (); + + fn try_from(value: Value<'a>) -> Result { + match value { + Value::PlainTime(idx) => Ok(idx), + _ => Err(()), + } + } +} +impl<'a> TryFrom> for TemporalPlainTime<'a> { + type Error = (); + + fn try_from(object: Object<'a>) -> Result { + match object { + Object::PlainTime(idx) => Ok(idx), + _ => Err(()), + } + } +} + +impl<'a> InternalSlots<'a> for TemporalPlainTime<'a> { + const DEFAULT_PROTOTYPE: ProtoIntrinsics = ProtoIntrinsics::TemporalPlainTime; + fn get_backing_object(self, agent: &Agent) -> Option> { + agent[self].object_index + } + fn set_backing_object(self, agent: &mut Agent, backing_object: OrdinaryObject<'static>) { + assert!(agent[self].object_index.replace(backing_object).is_none()); + } +} + +impl<'a> InternalMethods<'a> for TemporalPlainTime<'a> {} + +// TODO: get rid of Index impls, replace with get/get_mut/get_direct/get_direct_mut functions +impl Index> for Agent { + type Output = PlainTimeHeapData<'static>; + + fn index(&self, index: TemporalPlainTime<'_>) -> &Self::Output { + &self.heap.plain_times[index] + } +} + +impl IndexMut> for Agent { + fn index_mut(&mut self, index: TemporalPlainTime<'_>) -> &mut Self::Output { + &mut self.heap.plain_times[index] + } +} + +impl Index> for Vec> { + type Output = PlainTimeHeapData<'static>; + + fn index(&self, index: TemporalPlainTime<'_>) -> &Self::Output { + self.get(index.get_index()) + .expect("heap acess out of bounds") + } +} + +impl IndexMut> for Vec> { + fn index_mut(&mut self, index: TemporalPlainTime<'_>) -> &mut Self::Output { + self.get_mut(index.get_index()) + .expect("heap access out of bounds") + } +} + +impl Rootable for TemporalPlainTime<'_> { + type RootRepr = HeapRootRef; + + fn to_root_repr(value: Self) -> Result { + Err(HeapRootData::PlainTime(value.unbind())) + } + + fn from_root_repr(value: &Self::RootRepr) -> Result { + Err(*value) + } + + fn from_heap_ref(heap_ref: HeapRootRef) -> Self::RootRepr { + heap_ref + } + + fn from_heap_data(heap_data: HeapRootData) -> Option { + match heap_data { + HeapRootData::PlainTime(object) => Some(object), + _ => None, + } + } +} + +impl HeapMarkAndSweep for TemporalPlainTime<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + queues.plain_times.push(*self); + } + + fn sweep_values(&mut self, compactions: &CompactionLists) { + compactions.plain_times.shift_index(&mut self.0); + } +} + +impl HeapSweepWeakReference for TemporalPlainTime<'static> { + fn sweep_weak_reference(self, compactions: &CompactionLists) -> Option { + compactions.plain_times.shift_weak_index(self.0).map(Self) + } +} + +impl<'a> CreateHeapData, TemporalPlainTime<'a>> for Heap { + fn create(&mut self, data: PlainTimeHeapData<'a>) -> TemporalPlainTime<'a> { + self.plain_times.push(data.unbind()); + self.alloc_counter += core::mem::size_of::>(); + TemporalPlainTime(BaseIndex::last(&self.plain_times)) + } +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs new file mode 100644 index 000000000..0c260e60e --- /dev/null +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/data.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::engine::context::NoGcScope; +use crate::heap::{CompactionLists, HeapMarkAndSweep, WorkQueues}; +use crate::{ + ecmascript::types::OrdinaryObject, + engine::context::{bindable_handle, trivially_bindable}, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PlainTimeHeapData<'a> { + pub(crate) object_index: Option>, + pub(crate) plain_time: temporal_rs::PlainTime, +} + +impl PlainTimeHeapData<'_> { + pub fn default() -> Self { + Self { + object_index: None, + plain_time: temporal_rs::PlainTime::new(0, 0, 0, 0, 0, 0).unwrap(), + } + } +} + +trivially_bindable!(temporal_rs::PlainTime); +bindable_handle!(PlainTimeHeapData); + +impl HeapMarkAndSweep for PlainTimeHeapData<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + let Self { + object_index, + plain_time: _, + } = self; + + object_index.mark_values(queues); + } + fn sweep_values(&mut self, compactions: &CompactionLists) { + let Self { + object_index, + plain_time: _, + } = self; + + object_index.sweep_values(compactions); + } +} diff --git a/nova_vm/src/ecmascript/execution/realm.rs b/nova_vm/src/ecmascript/execution/realm.rs index 9b101de22..eda9bbdab 100644 --- a/nova_vm/src/ecmascript/execution/realm.rs +++ b/nova_vm/src/ecmascript/execution/realm.rs @@ -676,6 +676,9 @@ pub(crate) fn set_default_global_bindings<'a>( // 19.4.4 Reflect define_property!(intrinsic Reflect, reflect); + + #[cfg(feature = "temporal")] + define_property!(intrinsic Temporal, temporal); } // 3. Return global. diff --git a/nova_vm/src/ecmascript/execution/realm/intrinsics.rs b/nova_vm/src/ecmascript/execution/realm/intrinsics.rs index bee33fd46..c59519b33 100644 --- a/nova_vm/src/ecmascript/execution/realm/intrinsics.rs +++ b/nova_vm/src/ecmascript/execution/realm/intrinsics.rs @@ -39,6 +39,19 @@ use crate::ecmascript::builtins::structured_data::shared_array_buffer_objects::{ shared_array_buffer_constructor::SharedArrayBufferConstructor, shared_array_buffer_prototype::SharedArrayBufferPrototype, }; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::{ + Temporal, + duration::{ + duration_constructor::TemporalDurationConstructor, + duration_prototype::TemporalDurationPrototype, + }, + instant::{ + instant_constructor::TemporalInstantConstructor, + instant_prototype::TemporalInstantPrototype, + }, +}; + #[cfg(feature = "regexp")] use crate::ecmascript::builtins::text_processing::regexp_objects::{ regexp_constructor::RegExpConstructor, regexp_prototype::RegExpPrototype, @@ -227,6 +240,12 @@ pub enum ProtoIntrinsics { RegExpStringIterator, Symbol, SyntaxError, + #[cfg(feature = "temporal")] + TemporalInstant, + #[cfg(feature = "temporal")] + TemporalDuration, + #[cfg(feature = "temporal")] + TemporalPlainTime, TypeError, #[cfg(feature = "array-buffer")] Uint16Array, @@ -307,6 +326,17 @@ impl Intrinsics { BigIntConstructor::create_intrinsic(agent, realm); #[cfg(feature = "math")] MathObject::create_intrinsic(agent, realm, gc); + + #[cfg(feature = "temporal")] + Temporal::create_intrinsic(agent, realm, gc); + #[cfg(feature = "temporal")] + TemporalInstantPrototype::create_intrinsic(agent, realm, gc); + #[cfg(feature = "temporal")] + TemporalInstantConstructor::create_intrinsic(agent, realm, gc); + #[cfg(feature = "temporal")] + TemporalDurationPrototype::create_intrinsic(agent, realm, gc); + #[cfg(feature = "temporal")] + TemporalDurationConstructor::create_intrinsic(agent, realm, gc); #[cfg(feature = "date")] DatePrototype::create_intrinsic(agent, realm); #[cfg(feature = "date")] @@ -416,6 +446,12 @@ impl Intrinsics { ProtoIntrinsics::String => self.string().into(), ProtoIntrinsics::Symbol => self.symbol().into(), ProtoIntrinsics::SyntaxError => self.syntax_error().into(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalInstant => self.temporal_instant().into(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalDuration => self.temporal_duration().into(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalPlainTime => self.temporal_plain_time().into(), ProtoIntrinsics::TypeError => self.type_error().into(), ProtoIntrinsics::URIError => self.uri_error().into(), ProtoIntrinsics::AggregateError => self.aggregate_error().into(), @@ -505,6 +541,12 @@ impl Intrinsics { ProtoIntrinsics::String => self.string_prototype().into(), ProtoIntrinsics::Symbol => self.symbol_prototype().into(), ProtoIntrinsics::SyntaxError => self.syntax_error_prototype().into(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalInstant => self.temporal_instant_prototype().into(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalDuration => self.temporal_duration_prototype().into(), + #[cfg(feature = "temporal")] + ProtoIntrinsics::TemporalPlainTime => self.temporal_plain_time_prototype().into(), ProtoIntrinsics::TypeError => self.type_error_prototype().into(), ProtoIntrinsics::URIError => self.uri_error_prototype().into(), ProtoIntrinsics::AggregateError => self.aggregate_error_prototype().into(), @@ -1011,6 +1053,45 @@ impl Intrinsics { IntrinsicObjectIndexes::MathObject.get_backing_object(self.object_index_base) } + /// %Temporal% + pub(crate) const fn temporal(&self) -> OrdinaryObject<'static> { + IntrinsicObjectIndexes::Temporal.get_backing_object(self.object_index_base) + } + + /// %Temporal.Instant.Prototype% + pub(crate) const fn temporal_duration_prototype(&self) -> OrdinaryObject<'static> { + IntrinsicObjectIndexes::TemporalDurationPrototype.get_backing_object(self.object_index_base) + } + + /// %Temporal.Instant% + pub(crate) const fn temporal_duration(&self) -> BuiltinFunction<'static> { + IntrinsicConstructorIndexes::TemporalDuration + .get_builtin_function(self.builtin_function_index_base) + } + + /// %Temporal.Instant.Prototype% + pub(crate) const fn temporal_instant_prototype(&self) -> OrdinaryObject<'static> { + IntrinsicObjectIndexes::TemporalInstantPrototype.get_backing_object(self.object_index_base) + } + + /// %Temporal.Instant% + pub(crate) const fn temporal_instant(&self) -> BuiltinFunction<'static> { + IntrinsicConstructorIndexes::TemporalInstant + .get_builtin_function(self.builtin_function_index_base) + } + + /// %Temporal.PlainTime.prototype% + pub(crate) const fn temporal_plain_time_prototype(&self) -> OrdinaryObject<'static> { + IntrinsicObjectIndexes::TemporalPlainTimePrototype + .get_backing_object(self.object_index_base) + } + + /// %Temporal.PlainTime% + pub(crate) const fn temporal_plain_time(&self) -> BuiltinFunction<'static> { + IntrinsicConstructorIndexes::TemporalPlainTime + .get_builtin_function(self.builtin_function_index_base) + } + /// %Number.prototype% pub(crate) fn number_prototype(&self) -> PrimitiveObject<'static> { IntrinsicPrimitiveObjectIndexes::NumberPrototype diff --git a/nova_vm/src/ecmascript/execution/weak_key.rs b/nova_vm/src/ecmascript/execution/weak_key.rs index 051ab68ef..7dee2ee8e 100644 --- a/nova_vm/src/ecmascript/execution/weak_key.rs +++ b/nova_vm/src/ecmascript/execution/weak_key.rs @@ -14,6 +14,13 @@ use crate::ecmascript::types::DATE_DISCRIMINANT; use crate::ecmascript::types::{ WEAK_MAP_DISCRIMINANT, WEAK_REF_DISCRIMINANT, WEAK_SET_DISCRIMINANT, }; +#[cfg(feature = "temporal")] +use crate::ecmascript::{ + builtins::temporal::{ + duration::TemporalDuration, instant::TemporalInstant, plain_time::TemporalPlainTime, + }, + types::{DURATION_DISCRIMINANT, INSTANT_DISCRIMINANT, PLAIN_TIME_DISCRIMINANT}, +}; #[cfg(feature = "proposal-float16array")] use crate::ecmascript::{builtins::typed_array::Float16Array, types::FLOAT_16_ARRAY_DISCRIMINANT}; #[cfg(all(feature = "proposal-float16array", feature = "shared-array-buffer"))] @@ -141,6 +148,12 @@ pub(crate) enum WeakKey<'a> { Array(Array<'a>) = ARRAY_DISCRIMINANT, #[cfg(feature = "date")] Date(Date<'a>) = DATE_DISCRIMINANT, + #[cfg(feature = "temporal")] + Instant(TemporalInstant<'a>) = INSTANT_DISCRIMINANT, + #[cfg(feature = "temporal")] + Duration(TemporalDuration<'a>) = DURATION_DISCRIMINANT, + #[cfg(feature = "temporal")] + PlainTime(TemporalPlainTime<'a>) = PLAIN_TIME_DISCRIMINANT, Error(Error<'a>) = ERROR_DISCRIMINANT, FinalizationRegistry(FinalizationRegistry<'a>) = FINALIZATION_REGISTRY_DISCRIMINANT, Map(Map<'a>) = MAP_DISCRIMINANT, @@ -254,6 +267,12 @@ impl<'a> From> for Value<'a> { WeakKey::Array(d) => Self::Array(d), #[cfg(feature = "date")] WeakKey::Date(d) => Self::Date(d), + #[cfg(feature = "temporal")] + WeakKey::Instant(d) => Self::Instant(d), + #[cfg(feature = "temporal")] + WeakKey::Duration(d) => Self::Duration(d), + #[cfg(feature = "temporal")] + WeakKey::PlainTime(d) => Self::PlainTime(d), WeakKey::Error(d) => Self::Error(d), WeakKey::FinalizationRegistry(d) => Self::FinalizationRegistry(d), WeakKey::Map(d) => Self::Map(d), @@ -361,6 +380,12 @@ impl<'a> From> for WeakKey<'a> { Object::Array(d) => Self::Array(d), #[cfg(feature = "date")] Object::Date(d) => Self::Date(d), + #[cfg(feature = "temporal")] + Object::Instant(d) => Self::Instant(d), + #[cfg(feature = "temporal")] + Object::Duration(d) => Self::Duration(d), + #[cfg(feature = "temporal")] + Object::PlainTime(d) => Self::PlainTime(d), Object::Error(d) => Self::Error(d), Object::FinalizationRegistry(d) => Self::FinalizationRegistry(d), Object::Map(d) => Self::Map(d), @@ -472,6 +497,12 @@ impl<'a> TryFrom> for Object<'a> { WeakKey::Array(d) => Ok(Self::Array(d)), #[cfg(feature = "date")] WeakKey::Date(d) => Ok(Self::Date(d)), + #[cfg(feature = "temporal")] + WeakKey::Instant(d) => Ok(Self::Instant(d)), + #[cfg(feature = "temporal")] + WeakKey::Duration(d) => Ok(Self::Duration(d)), + #[cfg(feature = "temporal")] + WeakKey::PlainTime(d) => Ok(Self::PlainTime(d)), WeakKey::Error(d) => Ok(Self::Error(d)), WeakKey::FinalizationRegistry(d) => Ok(Self::FinalizationRegistry(d)), WeakKey::Map(d) => Ok(Self::Map(d)), @@ -614,6 +645,12 @@ impl HeapMarkAndSweep for WeakKey<'static> { Self::Array(d) => d.mark_values(queues), #[cfg(feature = "date")] Self::Date(d) => d.mark_values(queues), + #[cfg(feature = "temporal")] + Self::Instant(d) => d.mark_values(queues), + #[cfg(feature = "temporal")] + Self::Duration(d) => d.mark_values(queues), + #[cfg(feature = "temporal")] + Self::PlainTime(d) => d.mark_values(queues), Self::Error(d) => d.mark_values(queues), Self::FinalizationRegistry(d) => d.mark_values(queues), Self::Map(d) => d.mark_values(queues), @@ -719,6 +756,12 @@ impl HeapMarkAndSweep for WeakKey<'static> { Self::Array(d) => d.sweep_values(compactions), #[cfg(feature = "date")] Self::Date(d) => d.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::Instant(d) => d.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::Duration(d) => d.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::PlainTime(d) => d.sweep_values(compactions), Self::Error(d) => d.sweep_values(compactions), Self::FinalizationRegistry(d) => d.sweep_values(compactions), Self::Map(d) => d.sweep_values(compactions), @@ -840,6 +883,12 @@ impl HeapSweepWeakReference for WeakKey<'static> { Self::Array(data) => data.sweep_weak_reference(compactions).map(Self::Array), #[cfg(feature = "date")] Self::Date(data) => data.sweep_weak_reference(compactions).map(Self::Date), + #[cfg(feature = "temporal")] + Self::Instant(data) => data.sweep_weak_reference(compactions).map(Self::Instant), + #[cfg(feature = "temporal")] + Self::Duration(data) => data.sweep_weak_reference(compactions).map(Self::Duration), + #[cfg(feature = "temporal")] + Self::PlainTime(data) => data.sweep_weak_reference(compactions).map(Self::PlainTime), Self::Error(data) => data.sweep_weak_reference(compactions).map(Self::Error), Self::FinalizationRegistry(data) => data .sweep_weak_reference(compactions) diff --git a/nova_vm/src/ecmascript/types/language/bigint.rs b/nova_vm/src/ecmascript/types/language/bigint.rs index fdf76613a..606a7949f 100644 --- a/nova_vm/src/ecmascript/types/language/bigint.rs +++ b/nova_vm/src/ecmascript/types/language/bigint.rs @@ -231,6 +231,15 @@ impl<'a> BigInt<'a> { } } + #[inline] + pub(crate) fn from_num_bigint(agent: &mut Agent, value: num_bigint::BigInt) -> Self { + if let Ok(result) = SmallBigInt::try_from(&value) { + Self::SmallBigInt(result) + } else { + agent.heap.create(BigIntHeapData { data: value }) + } + } + pub fn try_into_i64(self, agent: &Agent) -> Result> { match self { BigInt::BigInt(b) => i64::try_from(&agent[b].data), @@ -238,12 +247,27 @@ impl<'a> BigInt<'a> { } } - #[inline] - pub(crate) fn from_num_bigint(agent: &mut Agent, value: num_bigint::BigInt) -> Self { - if let Ok(result) = SmallBigInt::try_from(&value) { - Self::SmallBigInt(result) - } else { - agent.heap.create(BigIntHeapData { data: value }) + pub fn try_into_i128(self, agent: &Agent) -> Option { + match self { + BigInt::BigInt(b) => { + let data = &agent[b].data; + let sign = data.sign(); + let mut digits = data.iter_u64_digits(); + if digits.len() > 2 { + return None; + } else if digits.len() == 1 { + let abs = digits.next().unwrap() as i128; + if sign == Sign::Minus { + return Some(abs.neg()); + } else { + return Some(abs); + } + } + // Hard part: check that u64 << 64 + u64 doesn't have an + // absolute value overflowing i128. + todo!(); + } + BigInt::SmallBigInt(b) => Some(b.into_i64() as i128), } } diff --git a/nova_vm/src/ecmascript/types/language/object.rs b/nova_vm/src/ecmascript/types/language/object.rs index 002444d9b..50904cf66 100644 --- a/nova_vm/src/ecmascript/types/language/object.rs +++ b/nova_vm/src/ecmascript/types/language/object.rs @@ -38,6 +38,13 @@ use super::{ use crate::ecmascript::builtins::date::Date; #[cfg(feature = "weak-refs")] use crate::ecmascript::builtins::{weak_map::WeakMap, weak_ref::WeakRef, weak_set::WeakSet}; +#[cfg(feature = "temporal")] +use crate::ecmascript::{ + builtins::temporal::{ + duration::TemporalDuration, instant::TemporalInstant, plain_time::TemporalPlainTime, + }, + types::{DURATION_DISCRIMINANT, INSTANT_DISCRIMINANT, PLAIN_TIME_DISCRIMINANT}, +}; #[cfg(feature = "proposal-float16array")] use crate::ecmascript::{builtins::typed_array::Float16Array, types::FLOAT_16_ARRAY_DISCRIMINANT}; #[cfg(all(feature = "proposal-float16array", feature = "shared-array-buffer"))] @@ -179,6 +186,12 @@ pub enum Object<'a> { Array(Array<'a>) = ARRAY_DISCRIMINANT, #[cfg(feature = "date")] Date(Date<'a>) = DATE_DISCRIMINANT, + #[cfg(feature = "temporal")] + Instant(TemporalInstant<'a>) = INSTANT_DISCRIMINANT, + #[cfg(feature = "temporal")] + Duration(TemporalDuration<'a>) = DURATION_DISCRIMINANT, + #[cfg(feature = "temporal")] + PlainTime(TemporalPlainTime<'a>) = PLAIN_TIME_DISCRIMINANT, Error(Error<'a>) = ERROR_DISCRIMINANT, FinalizationRegistry(FinalizationRegistry<'a>) = FINALIZATION_REGISTRY_DISCRIMINANT, Map(Map<'a>) = MAP_DISCRIMINANT, @@ -777,6 +790,12 @@ impl<'a> From> for Value<'a> { Object::Array(data) => Self::Array(data), #[cfg(feature = "date")] Object::Date(data) => Self::Date(data), + #[cfg(feature = "temporal")] + Object::Instant(data) => Value::Instant(data), + #[cfg(feature = "temporal")] + Object::Duration(data) => Value::Duration(data), + #[cfg(feature = "temporal")] + Object::PlainTime(data) => Value::PlainTime(data), Object::Error(data) => Self::Error(data), Object::FinalizationRegistry(data) => Self::FinalizationRegistry(data), Object::Map(data) => Self::Map(data), @@ -885,6 +904,12 @@ impl<'a> TryFrom> for Object<'a> { Value::Array(x) => Ok(Self::from(x)), #[cfg(feature = "date")] Value::Date(x) => Ok(Self::Date(x)), + #[cfg(feature = "temporal")] + Value::Instant(x) => Ok(Self::Instant(x)), + #[cfg(feature = "temporal")] + Value::Duration(x) => Ok(Self::Duration(x)), + #[cfg(feature = "temporal")] + Value::PlainTime(x) => Ok(Self::PlainTime(x)), Value::Error(x) => Ok(Self::from(x)), Value::BoundFunction(x) => Ok(Self::from(x)), Value::BuiltinFunction(x) => Ok(Self::from(x)), @@ -1001,6 +1026,12 @@ macro_rules! object_delegate { Self::Array(data) => data.$method($($arg),+), #[cfg(feature = "date")] Self::Date(data) => data.$method($($arg),+), + #[cfg(feature = "temporal")] + Object::Instant(data) => data.$method($($arg),+), + #[cfg(feature = "temporal")] + Object::Duration(data) => data.$method($($arg),+), + #[cfg(feature = "temporal")] + Object::PlainTime(data) => data.$method($($arg),+), Self::Error(data) => data.$method($($arg),+), Self::BoundFunction(data) => data.$method($($arg),+), Self::BuiltinFunction(data) => data.$method($($arg),+), @@ -1431,6 +1462,12 @@ impl HeapSweepWeakReference for Object<'static> { Self::Array(data) => data.sweep_weak_reference(compactions).map(Self::Array), #[cfg(feature = "date")] Self::Date(data) => data.sweep_weak_reference(compactions).map(Self::Date), + #[cfg(feature = "temporal")] + Self::Instant(data) => data.sweep_weak_reference(compactions).map(Self::Instant), + #[cfg(feature = "temporal")] + Self::Duration(data) => data.sweep_weak_reference(compactions).map(Self::Duration), + #[cfg(feature = "temporal")] + Self::PlainTime(data) => data.sweep_weak_reference(compactions).map(Self::PlainTime), Self::Error(data) => data.sweep_weak_reference(compactions).map(Self::Error), Self::BoundFunction(data) => data .sweep_weak_reference(compactions) @@ -1667,6 +1704,12 @@ impl TryFrom for Object<'_> { HeapRootData::Array(array) => Ok(Self::Array(array)), #[cfg(feature = "date")] HeapRootData::Date(date) => Ok(Self::Date(date)), + #[cfg(feature = "temporal")] + HeapRootData::Instant(instant) => Ok(Self::Instant(instant)), + #[cfg(feature = "temporal")] + HeapRootData::Duration(duration) => Ok(Self::Duration(duration)), + #[cfg(feature = "temporal")] + HeapRootData::PlainTime(plain_time) => Ok(Self::PlainTime(plain_time)), HeapRootData::Error(error) => Ok(Self::Error(error)), HeapRootData::FinalizationRegistry(finalization_registry) => { Ok(Self::FinalizationRegistry(finalization_registry)) diff --git a/nova_vm/src/ecmascript/types/language/value.rs b/nova_vm/src/ecmascript/types/language/value.rs index 9e7480c1b..89a240eb8 100644 --- a/nova_vm/src/ecmascript/types/language/value.rs +++ b/nova_vm/src/ecmascript/types/language/value.rs @@ -8,6 +8,10 @@ use super::{ }; #[cfg(feature = "date")] use crate::ecmascript::builtins::date::Date; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::{ + duration::TemporalDuration, instant::TemporalInstant, plain_time::TemporalPlainTime, +}; #[cfg(feature = "proposal-float16array")] use crate::ecmascript::builtins::typed_array::Float16Array; #[cfg(all(feature = "proposal-float16array", feature = "shared-array-buffer"))] @@ -178,6 +182,12 @@ pub enum Value<'a> { Array(Array<'a>), #[cfg(feature = "date")] Date(Date<'a>), + #[cfg(feature = "temporal")] + Instant(TemporalInstant<'a>), + #[cfg(feature = "temporal")] + Duration(TemporalDuration<'a>), + #[cfg(feature = "temporal")] + PlainTime(TemporalPlainTime<'a>), Error(Error<'a>), FinalizationRegistry(FinalizationRegistry<'a>), Map(Map<'a>), @@ -314,6 +324,15 @@ pub(crate) const OBJECT_DISCRIMINANT: u8 = pub(crate) const ARRAY_DISCRIMINANT: u8 = value_discriminant(Value::Array(Array::_def())); #[cfg(feature = "date")] pub(crate) const DATE_DISCRIMINANT: u8 = value_discriminant(Value::Date(Date::_def())); +#[cfg(feature = "temporal")] +pub(crate) const INSTANT_DISCRIMINANT: u8 = + value_discriminant(Value::Instant(TemporalInstant::_def())); +#[cfg(feature = "temporal")] +pub(crate) const DURATION_DISCRIMINANT: u8 = + value_discriminant(Value::Duration(TemporalDuration::_def())); +#[cfg(feature = "temporal")] +pub(crate) const PLAIN_TIME_DISCRIMINANT: u8 = + value_discriminant(Value::PlainTime(TemporalPlainTime::_def())); pub(crate) const ERROR_DISCRIMINANT: u8 = value_discriminant(Value::Error(Error::_def())); pub(crate) const BUILTIN_FUNCTION_DISCRIMINANT: u8 = value_discriminant(Value::BuiltinFunction(BuiltinFunction::_def())); @@ -988,6 +1007,12 @@ impl Rootable for Value<'_> { Self::Array(array) => Err(HeapRootData::Array(array.unbind())), #[cfg(feature = "date")] Self::Date(date) => Err(HeapRootData::Date(date.unbind())), + #[cfg(feature = "temporal")] + Self::Instant(instant) => Err(HeapRootData::Instant(instant.unbind())), + #[cfg(feature = "temporal")] + Self::Duration(duration) => Err(HeapRootData::Duration(duration.unbind())), + #[cfg(feature = "temporal")] + Self::PlainTime(plain_time) => Err(HeapRootData::PlainTime(plain_time.unbind())), Self::Error(error) => Err(HeapRootData::Error(error.unbind())), Self::FinalizationRegistry(finalization_registry) => Err( HeapRootData::FinalizationRegistry(finalization_registry.unbind()), @@ -1149,6 +1174,12 @@ impl Rootable for Value<'_> { HeapRootData::Array(array) => Some(Self::Array(array)), #[cfg(feature = "date")] HeapRootData::Date(date) => Some(Self::Date(date)), + #[cfg(feature = "temporal")] + HeapRootData::Instant(instant) => Some(Self::Instant(instant)), + #[cfg(feature = "temporal")] + HeapRootData::Duration(duration) => Some(Self::Duration(duration)), + #[cfg(feature = "temporal")] + HeapRootData::PlainTime(plain_time) => Some(Self::PlainTime(plain_time)), HeapRootData::Error(error) => Some(Self::Error(error)), HeapRootData::FinalizationRegistry(finalization_registry) => { Some(Self::FinalizationRegistry(finalization_registry)) @@ -1297,6 +1328,12 @@ impl HeapMarkAndSweep for Value<'static> { Self::Array(data) => data.mark_values(queues), #[cfg(feature = "date")] Self::Date(dv) => dv.mark_values(queues), + #[cfg(feature = "temporal")] + Self::Instant(data) => data.mark_values(queues), + #[cfg(feature = "temporal")] + Self::Duration(data) => data.mark_values(queues), + #[cfg(feature = "temporal")] + Self::PlainTime(data) => data.mark_values(queues), Self::Error(data) => data.mark_values(queues), Self::BoundFunction(data) => data.mark_values(queues), Self::BuiltinFunction(data) => data.mark_values(queues), @@ -1317,7 +1354,6 @@ impl HeapMarkAndSweep for Value<'static> { Self::WeakRef(data) => data.mark_values(queues), #[cfg(feature = "weak-refs")] Self::WeakSet(data) => data.mark_values(queues), - #[cfg(feature = "array-buffer")] Self::ArrayBuffer(ab) => ab.mark_values(queues), #[cfg(feature = "array-buffer")] @@ -1346,7 +1382,6 @@ impl HeapMarkAndSweep for Value<'static> { Self::Float32Array(ta) => ta.mark_values(queues), #[cfg(feature = "array-buffer")] Self::Float64Array(ta) => ta.mark_values(queues), - #[cfg(feature = "shared-array-buffer")] Self::SharedArrayBuffer(data) => data.mark_values(queues), #[cfg(feature = "shared-array-buffer")] @@ -1375,7 +1410,6 @@ impl HeapMarkAndSweep for Value<'static> { Self::SharedFloat32Array(sta) => sta.mark_values(queues), #[cfg(feature = "shared-array-buffer")] Self::SharedFloat64Array(sta) => sta.mark_values(queues), - Self::BuiltinConstructorFunction(data) => data.mark_values(queues), Self::BuiltinPromiseResolvingFunction(data) => data.mark_values(queues), Self::BuiltinPromiseFinallyFunction(data) => data.mark_values(queues), @@ -1414,6 +1448,12 @@ impl HeapMarkAndSweep for Value<'static> { Self::Array(data) => data.sweep_values(compactions), #[cfg(feature = "date")] Self::Date(data) => data.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::Instant(data) => data.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::Duration(data) => data.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::PlainTime(data) => data.sweep_values(compactions), Self::Error(data) => data.sweep_values(compactions), Self::BoundFunction(data) => data.sweep_values(compactions), Self::BuiltinFunction(data) => data.sweep_values(compactions), @@ -1565,6 +1605,12 @@ fn map_object_to_static_string_repr(value: Value) -> String<'static> { Object::SharedFloat16Array(_) => BUILTIN_STRING_MEMORY._object_Object_, #[cfg(feature = "date")] Object::Date(_) => BUILTIN_STRING_MEMORY._object_Object_, + #[cfg(feature = "temporal")] + Object::Instant(_) => BUILTIN_STRING_MEMORY._object_Object_, + #[cfg(feature = "temporal")] + Object::Duration(_) => BUILTIN_STRING_MEMORY._object_Object_, + #[cfg(feature = "temporal")] + Object::PlainTime(_) => BUILTIN_STRING_MEMORY._object_Object_, #[cfg(feature = "set")] Object::Set(_) | Object::SetIterator(_) => BUILTIN_STRING_MEMORY._object_Object_, #[cfg(feature = "weak-refs")] diff --git a/nova_vm/src/engine/bytecode/vm.rs b/nova_vm/src/engine/bytecode/vm.rs index f82f026e3..deeb7363a 100644 --- a/nova_vm/src/engine/bytecode/vm.rs +++ b/nova_vm/src/engine/bytecode/vm.rs @@ -1326,6 +1326,12 @@ fn typeof_operator(agent: &Agent, val: Value, gc: NoGcScope) -> String<'static> Value::SharedFloat16Array(_) => BUILTIN_STRING_MEMORY.object, #[cfg(feature = "date")] Value::Date(_) => BUILTIN_STRING_MEMORY.object, + #[cfg(feature = "temporal")] + Value::Instant(_) => BUILTIN_STRING_MEMORY.object, + #[cfg(feature = "temporal")] + Value::Duration(_) => BUILTIN_STRING_MEMORY.object, + #[cfg(feature = "temporal")] + Value::PlainTime(_) => BUILTIN_STRING_MEMORY.object, // 13. If val has a [[Call]] internal slot, return "function". Value::BoundFunction(_) | Value::BuiltinFunction(_) | Value::ECMAScriptFunction(_) | Value::BuiltinConstructorFunction(_) | diff --git a/nova_vm/src/engine/rootable.rs b/nova_vm/src/engine/rootable.rs index 3bf1bfa3b..0bd8c3e8c 100644 --- a/nova_vm/src/engine/rootable.rs +++ b/nova_vm/src/engine/rootable.rs @@ -17,6 +17,13 @@ use crate::ecmascript::types::DATE_DISCRIMINANT; use crate::ecmascript::types::{ WEAK_MAP_DISCRIMINANT, WEAK_REF_DISCRIMINANT, WEAK_SET_DISCRIMINANT, }; +#[cfg(feature = "temporal")] +use crate::ecmascript::{ + builtins::temporal::{ + duration::TemporalDuration, instant::TemporalInstant, plain_time::TemporalPlainTime, + }, + types::{DURATION_DISCRIMINANT, INSTANT_DISCRIMINANT, PLAIN_TIME_DISCRIMINANT}, +}; #[cfg(feature = "proposal-float16array")] use crate::ecmascript::{builtins::typed_array::Float16Array, types::FLOAT_16_ARRAY_DISCRIMINANT}; #[cfg(all(feature = "proposal-float16array", feature = "shared-array-buffer"))] @@ -135,6 +142,10 @@ pub mod private { #[cfg(feature = "date")] use crate::ecmascript::builtins::date::Date; + #[cfg(feature = "temporal")] + use crate::ecmascript::builtins::temporal::{ + duration::TemporalDuration, instant::TemporalInstant, plain_time::TemporalPlainTime, + }; #[cfg(feature = "shared-array-buffer")] use crate::ecmascript::builtins::{ data_view::SharedDataView, @@ -230,6 +241,12 @@ pub mod private { impl RootableSealed for BuiltinPromiseFinallyFunction<'_> {} #[cfg(feature = "date")] impl RootableSealed for Date<'_> {} + #[cfg(feature = "temporal")] + impl RootableSealed for TemporalInstant<'_> {} + #[cfg(feature = "temporal")] + impl RootableSealed for TemporalDuration<'_> {} + #[cfg(feature = "temporal")] + impl RootableSealed for TemporalPlainTime<'_> {} impl RootableSealed for ECMAScriptFunction<'_> {} impl RootableSealed for EmbedderObject<'_> {} impl RootableSealed for Error<'_> {} @@ -544,6 +561,12 @@ pub enum HeapRootData { Array(Array<'static>) = ARRAY_DISCRIMINANT, #[cfg(feature = "date")] Date(Date<'static>) = DATE_DISCRIMINANT, + #[cfg(feature = "temporal")] + Instant(TemporalInstant<'static>) = INSTANT_DISCRIMINANT, + #[cfg(feature = "temporal")] + Duration(TemporalDuration<'static>) = DURATION_DISCRIMINANT, + #[cfg(feature = "temporal")] + PlainTime(TemporalPlainTime<'static>) = PLAIN_TIME_DISCRIMINANT, Error(Error<'static>) = ERROR_DISCRIMINANT, FinalizationRegistry(FinalizationRegistry<'static>) = FINALIZATION_REGISTRY_DISCRIMINANT, Map(Map<'static>) = MAP_DISCRIMINANT, @@ -678,6 +701,12 @@ impl From> for HeapRootData { Object::Array(array) => Self::Array(array), #[cfg(feature = "date")] Object::Date(date) => Self::Date(date), + #[cfg(feature = "temporal")] + Object::Instant(instant) => Self::Instant(instant), + #[cfg(feature = "temporal")] + Object::Duration(duration) => Self::Duration(duration), + #[cfg(feature = "temporal")] + Object::PlainTime(plain_time) => Self::PlainTime(plain_time), Object::Error(error) => Self::Error(error), Object::FinalizationRegistry(finalization_registry) => { Self::FinalizationRegistry(finalization_registry) @@ -837,6 +866,12 @@ impl HeapMarkAndSweep for HeapRootData { Self::Array(array) => array.mark_values(queues), #[cfg(feature = "date")] Self::Date(date) => date.mark_values(queues), + #[cfg(feature = "temporal")] + Self::Instant(instant) => instant.mark_values(queues), + #[cfg(feature = "temporal")] + Self::Duration(duration) => duration.mark_values(queues), + #[cfg(feature = "temporal")] + Self::PlainTime(plain_time) => plain_time.mark_values(queues), Self::Error(error) => error.mark_values(queues), Self::FinalizationRegistry(finalization_registry) => { finalization_registry.mark_values(queues) @@ -986,6 +1021,12 @@ impl HeapMarkAndSweep for HeapRootData { Self::Array(array) => array.sweep_values(compactions), #[cfg(feature = "date")] Self::Date(date) => date.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::Instant(instant) => instant.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::Duration(duration) => duration.sweep_values(compactions), + #[cfg(feature = "temporal")] + Self::PlainTime(plain_time) => plain_time.sweep_values(compactions), Self::Error(error) => error.sweep_values(compactions), Self::FinalizationRegistry(finalization_registry) => { finalization_registry.sweep_values(compactions) diff --git a/nova_vm/src/heap.rs b/nova_vm/src/heap.rs index b93a43a8e..0ecfcce5a 100644 --- a/nova_vm/src/heap.rs +++ b/nova_vm/src/heap.rs @@ -29,6 +29,11 @@ pub(crate) use self::heap_constants::{ pub(crate) use self::object_entry::{ObjectEntry, ObjectEntryPropertyDescriptor}; #[cfg(feature = "date")] use crate::ecmascript::builtins::date::data::DateHeapData; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::{ + duration::data::DurationHeapData, instant::data::InstantRecord, + plain_time::data::PlainTimeHeapData, +}; #[cfg(feature = "array-buffer")] use crate::ecmascript::builtins::{ ArrayBuffer, ArrayBufferHeapData, @@ -145,6 +150,12 @@ pub(crate) struct Heap { pub(crate) caches: Caches<'static>, #[cfg(feature = "date")] pub(crate) dates: Vec>, + #[cfg(feature = "temporal")] + pub(crate) instants: Vec>, + #[cfg(feature = "temporal")] + pub(crate) durations: Vec>, + #[cfg(feature = "temporal")] + pub(crate) plain_times: Vec>, pub(crate) ecmascript_functions: Vec>, /// ElementsArrays is where all keys and values arrays live; /// Element arrays are static arrays of Values plus @@ -292,6 +303,13 @@ impl Heap { caches: Caches::with_capacity(1024), #[cfg(feature = "date")] dates: Vec::with_capacity(1024), + // TODO: assign appropriate value for Temporal objects. + #[cfg(feature = "temporal")] + instants: Vec::with_capacity(1024), + #[cfg(feature = "temporal")] + durations: Vec::with_capacity(1024), + #[cfg(feature = "temporal")] + plain_times: Vec::with_capacity(1024), ecmascript_functions: Vec::with_capacity(1024), elements: ElementArrays { e2pow1: ElementArray2Pow1::with_capacity(1024), diff --git a/nova_vm/src/heap/heap_bits.rs b/nova_vm/src/heap/heap_bits.rs index 378b64b84..fb082552c 100644 --- a/nova_vm/src/heap/heap_bits.rs +++ b/nova_vm/src/heap/heap_bits.rs @@ -25,6 +25,10 @@ use super::{ }; #[cfg(feature = "date")] use crate::ecmascript::builtins::date::Date; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::{ + duration::TemporalDuration, instant::TemporalInstant, plain_time::TemporalPlainTime, +}; #[cfg(feature = "array-buffer")] use crate::ecmascript::builtins::{ArrayBuffer, data_view::DataView, typed_array::VoidArray}; #[cfg(feature = "shared-array-buffer")] @@ -453,6 +457,12 @@ pub(crate) struct HeapBits { pub(super) data_views: BitRange, #[cfg(feature = "date")] pub(super) dates: BitRange, + #[cfg(feature = "temporal")] + pub(super) instants: BitRange, + #[cfg(feature = "temporal")] + pub(super) plain_times: BitRange, + #[cfg(feature = "temporal")] + pub(super) durations: BitRange, pub(super) declarative_environments: BitRange, pub(super) ecmascript_functions: BitRange, pub(super) embedder_objects: BitRange, @@ -529,6 +539,12 @@ pub(crate) struct WorkQueues<'a> { pub(crate) data_views: Vec>, #[cfg(feature = "date")] pub(crate) dates: Vec>, + #[cfg(feature = "temporal")] + pub(crate) instants: Vec>, + #[cfg(feature = "temporal")] + pub(crate) plain_times: Vec>, + #[cfg(feature = "temporal")] + pub(crate) durations: Vec>, pub(crate) declarative_environments: Vec>, pub(crate) e_2_1: Vec>, pub(crate) e_2_2: Vec>, @@ -678,6 +694,11 @@ impl HeapBits { let data_views = BitRange::from_bit_count_and_len(&mut bit_count, heap.data_views.len()); #[cfg(feature = "date")] let dates = BitRange::from_bit_count_and_len(&mut bit_count, heap.dates.len()); + #[cfg(feature = "date")] + let instants = BitRange::from_bit_count_and_len(&mut bit_count, heap.instants.len()); + #[cfg(feature = "date")] + let plain_times = BitRange::from_bit_count_and_len(&mut bit_count, heap.plain_times.len()); + let durations = BitRange::from_bit_count_and_len(&mut bit_count, heap.durations.len()); let declarative_environments = BitRange::from_bit_count_and_len(&mut bit_count, heap.environments.declarative.len()); let ecmascript_functions = @@ -785,6 +806,12 @@ impl HeapBits { data_views, #[cfg(feature = "date")] dates, + #[cfg(feature = "temporal")] + instants, + #[cfg(feature = "temporal")] + plain_times, + #[cfg(feature = "temporal")] + durations, declarative_environments, e_2_1, e_2_2, @@ -895,6 +922,12 @@ impl HeapBits { WeakKey::Array(d) => self.arrays.get_bit(d.get_index(), &self.bits), #[cfg(feature = "date")] WeakKey::Date(d) => self.dates.get_bit(d.get_index(), &self.bits), + #[cfg(feature = "temporal")] + WeakKey::Instant(d) => self.instants.get_bit(d.get_index(), &self.bits), + #[cfg(feature = "temporal")] + WeakKey::PlainTime(d) => self.plain_times.get_bit(d.get_index(), &self.bits), + #[cfg(feature = "temporal")] + WeakKey::Duration(d) => self.durations.get_bit(d.get_index(), &self.bits), WeakKey::Error(d) => self.errors.get_bit(d.get_index(), &self.bits), WeakKey::FinalizationRegistry(d) => self .finalization_registrys @@ -1032,6 +1065,12 @@ impl<'a> WorkQueues<'a> { data_views: Vec::with_capacity(heap.data_views.len() / 4), #[cfg(feature = "date")] dates: Vec::with_capacity(heap.dates.len() / 4), + #[cfg(feature = "temporal")] + instants: Vec::with_capacity(heap.instants.len() / 4), + #[cfg(feature = "temporal")] + durations: Vec::with_capacity(heap.durations.len() / 4), + #[cfg(feature = "temporal")] + plain_times: Vec::with_capacity(heap.plain_times.len() / 4), declarative_environments: Vec::with_capacity(heap.environments.declarative.len() / 4), e_2_1: Vec::with_capacity(heap.elements.e2pow1.values.len() / 4), e_2_2: Vec::with_capacity(heap.elements.e2pow2.values.len() / 4), @@ -1138,6 +1177,12 @@ impl<'a> WorkQueues<'a> { data_views, #[cfg(feature = "date")] dates, + #[cfg(feature = "temporal")] + instants, + #[cfg(feature = "temporal")] + durations, + #[cfg(feature = "temporal")] + plain_times, declarative_environments, e_2_1, e_2_2, @@ -1217,6 +1262,7 @@ impl<'a> WorkQueues<'a> { weak_sets, } = self; + #[cfg(not(feature = "temporal"))] #[cfg(not(feature = "date"))] let dates: &[bool; 0] = &[]; #[cfg(not(feature = "array-buffer"))] @@ -1257,6 +1303,9 @@ impl<'a> WorkQueues<'a> { && caches.is_empty() && data_views.is_empty() && dates.is_empty() + && instants.is_empty() + && durations.is_empty() + && plain_times.is_empty() && declarative_environments.is_empty() && e_2_1.is_empty() && e_2_2.is_empty() @@ -1616,6 +1665,12 @@ pub(crate) struct CompactionLists { pub(crate) data_views: CompactionList, #[cfg(feature = "date")] pub(crate) dates: CompactionList, + #[cfg(feature = "temporal")] + pub(crate) instants: CompactionList, + #[cfg(feature = "temporal")] + pub(crate) plain_times: CompactionList, + #[cfg(feature = "temporal")] + pub(crate) durations: CompactionList, pub(crate) declarative_environments: CompactionList, pub(crate) e_2_1: CompactionList, pub(crate) e_2_2: CompactionList, @@ -1771,6 +1826,12 @@ impl CompactionLists { source_codes: CompactionList::from_mark_bits(&bits.source_codes, &bits.bits), #[cfg(feature = "date")] dates: CompactionList::from_mark_bits(&bits.dates, &bits.bits), + #[cfg(feature = "temporal")] + instants: CompactionList::from_mark_bits(&bits.instants, &bits.bits), + #[cfg(feature = "temporal")] + plain_times: CompactionList::from_mark_bits(&bits.plain_times, &bits.bits), + #[cfg(feature = "temporal")] + durations: CompactionList::from_mark_bits(&bits.durations, &bits.bits), errors: CompactionList::from_mark_bits(&bits.errors, &bits.bits), executables: CompactionList::from_mark_bits(&bits.executables, &bits.bits), maps: CompactionList::from_mark_bits(&bits.maps, &bits.bits), diff --git a/nova_vm/src/heap/heap_constants.rs b/nova_vm/src/heap/heap_constants.rs index 0afac0dfa..9b97a4015 100644 --- a/nova_vm/src/heap/heap_constants.rs +++ b/nova_vm/src/heap/heap_constants.rs @@ -34,6 +34,14 @@ pub(crate) enum IntrinsicObjectIndexes { MathObject, #[cfg(feature = "date")] DatePrototype, + #[cfg(feature = "temporal")] + Temporal, + #[cfg(feature = "temporal")] + TemporalInstantPrototype, + #[cfg(feature = "temporal")] + TemporalDurationPrototype, + #[cfg(feature = "temporal")] + TemporalPlainTimePrototype, // Text processing #[cfg(feature = "regexp")] @@ -171,6 +179,12 @@ pub(crate) enum IntrinsicConstructorIndexes { BigInt, #[cfg(feature = "date")] Date, + #[cfg(feature = "temporal")] + TemporalInstant, + #[cfg(feature = "temporal")] + TemporalDuration, + #[cfg(feature = "temporal")] + TemporalPlainTime, // Text processing String, diff --git a/nova_vm/src/heap/heap_gc.rs b/nova_vm/src/heap/heap_gc.rs index 6acdd03e8..c81e5fd02 100644 --- a/nova_vm/src/heap/heap_gc.rs +++ b/nova_vm/src/heap/heap_gc.rs @@ -18,6 +18,12 @@ use super::{ }; #[cfg(feature = "date")] use crate::ecmascript::builtins::date::Date; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::duration::TemporalDuration; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::instant::TemporalInstant; +#[cfg(feature = "temporal")] +use crate::ecmascript::builtins::temporal::plain_time::TemporalPlainTime; #[cfg(feature = "array-buffer")] use crate::ecmascript::builtins::{ArrayBuffer, data_view::DataView, typed_array::VoidArray}; #[cfg(feature = "shared-array-buffer")] @@ -128,6 +134,12 @@ pub fn heap_gc(agent: &mut Agent, root_realms: &mut [Option>], gc caches, #[cfg(feature = "date")] dates, + #[cfg(feature = "temporal")] + instants, + #[cfg(feature = "temporal")] + durations, + #[cfg(feature = "temporal")] + plain_times, ecmascript_functions, elements, embedder_objects, @@ -548,6 +560,37 @@ pub fn heap_gc(agent: &mut Agent, root_realms: &mut [Option>], gc } }); } + #[cfg(feature = "temporal")] + { + let mut instant_marks: Box<[TemporalInstant]> = queues.instants.drain(..).collect(); + instant_marks.sort(); + instant_marks.iter().for_each(|&idx| { + let index = idx.get_index(); + if bits.instants.set_bit(index, &bits.bits) { + // Did mark. + instants.get(index).mark_values(&mut queues); + } + }); + let mut duration_marks: Box<[TemporalDuration]> = queues.durations.drain(..).collect(); + duration_marks.sort(); + duration_marks.iter().for_each(|&idx| { + let index = idx.get_index(); + if bits.durations.set_bit(index, &bits.bits) { + // Did mark. + durations.get(index).mark_values(&mut queues); + } + }); + let mut plain_time_marks: Box<[TemporalPlainTime]> = + queues.plain_times.drain(..).collect(); + plain_time_marks.sort(); + plain_time_marks.iter().for_each(|&idx| { + let index = idx.get_index(); + if bits.plain_times.set_bit(index, &bits.bits) { + // Did mark. + plain_times.get(index).mark_values(&mut queues); + } + }); + } if !queues.embedder_objects.is_empty() { let mut embedder_object_marks: Box<[EmbedderObject]> = @@ -1251,6 +1294,12 @@ fn sweep( caches, #[cfg(feature = "date")] dates, + #[cfg(feature = "temporal")] + instants, + #[cfg(feature = "temporal")] + durations, + #[cfg(feature = "temporal")] + plain_times, ecmascript_functions, elements, embedder_objects, @@ -1704,6 +1753,24 @@ fn sweep( sweep_heap_vector_values(dates, &compactions, &bits.dates, &bits.bits); }); } + #[cfg(feature = "temporal")] + if !instants.is_empty() { + s.spawn(|| { + sweep_heap_vector_values(instants, &compactions, &bits.instants, &bits.bits); + }); + } + #[cfg(feature = "temporal")] + if !durations.is_empty() { + s.spawn(|| { + sweep_heap_vector_values(durations, &compactions, &bits.durations, &bits.bits); + }); + } + #[cfg(feature = "temporal")] + if !plain_times.is_empty() { + s.spawn(|| { + sweep_heap_vector_values(plain_times, &compactions, &bits.plain_times, &bits.bits); + }); + } if !declarative.is_empty() { s.spawn(|| { sweep_heap_vector_values(