Browse Source

`Date` refactor (#3595)

* Refactor date builtin

* Remove chrono dependency

* Replace custom modulo with rem_euclid
pull/3607/head
raskad 10 months ago committed by GitHub
parent
commit
0ed7da5730
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 86
      Cargo.lock
  2. 2
      Cargo.toml
  3. 2
      core/engine/Cargo.toml
  4. 1559
      core/engine/src/builtins/date/mod.rs
  5. 143
      core/engine/src/builtins/date/tests.rs
  6. 946
      core/engine/src/builtins/date/utils.rs
  7. 58
      core/engine/src/context/hooks.rs
  8. 11
      core/engine/src/object/builtins/jsdate.rs
  9. 23
      core/engine/src/value/integer.rs
  10. 18
      core/engine/src/value/mod.rs
  11. 2
      examples/Cargo.toml
  12. 15
      examples/src/bin/jsdate.rs
  13. 1
      ffi/wasm/Cargo.toml
  14. 1
      ffi/wasm/src/lib.rs
  15. 1
      tests/tester/Cargo.toml
  16. 6
      tests/tester/src/main.rs

86
Cargo.lock generated

@ -49,21 +49,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
@ -403,7 +388,6 @@ dependencies = [
"boa_temporal",
"bytemuck",
"cfg-if",
"chrono",
"criterion",
"dashmap",
"fast-float",
@ -448,6 +432,7 @@ dependencies = [
"textwrap",
"thin-vec",
"thiserror",
"time 0.3.31",
"web-time",
"writeable",
"yoke",
@ -464,9 +449,9 @@ dependencies = [
"boa_interner",
"boa_parser",
"boa_runtime",
"chrono",
"futures-util",
"smol",
"time 0.3.31",
]
[[package]]
@ -600,6 +585,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serde_yaml",
"time 0.3.31",
"toml 0.8.8",
]
@ -608,7 +594,6 @@ name = "boa_wasm"
version = "0.17.0"
dependencies = [
"boa_engine",
"chrono",
"console_error_panic_hook",
"getrandom",
"wasm-bindgen",
@ -710,20 +695,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.48.5",
]
[[package]]
name = "ciborium"
version = "0.2.1"
@ -886,12 +857,6 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "core_maths"
version = "0.1.0"
@ -1569,29 +1534,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_calendar"
version = "1.4.0"
@ -3193,7 +3135,7 @@ checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1"
dependencies = [
"colored",
"log",
"time 0.3.30",
"time 0.3.31",
"windows-sys 0.48.0",
]
@ -3508,18 +3450,19 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
dependencies = [
"deranged",
"itoa",
"js-sys",
"libc",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros 0.2.15",
"time-macros 0.2.16",
]
[[package]]
@ -3540,9 +3483,9 @@ dependencies = [
[[package]]
name = "time-macros"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
dependencies = [
"time-core",
]
@ -4265,15 +4208,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.33.0"

2
Cargo.toml

@ -46,7 +46,6 @@ boa_temporal = { version = "~0.17.0", path = "core/temporal" }
# Shared deps
arbitrary = "1"
bitflags = "2.4.2"
chrono = { version = "0.4.31", default-features = false }
clap = "4.4.18"
colored = "2.1.0"
fast-float = "0.2.0"
@ -67,6 +66,7 @@ serde = "1.0.195"
static_assertions = "1.1.0"
textwrap = "0.16.0"
thin-vec = "0.2.13"
time = {version = "0.3.31", no-default-features = true, features = ["local-offset", "large-dates", "wasm-bindgen", "parsing", "formatting", "macros"]}
# ICU4X

2
core/engine/Cargo.toml

@ -74,7 +74,6 @@ num-integer = "0.1.45"
bitflags.workspace = true
indexmap = { workspace = true, features = ["std"] }
ryu-js = "1.0.0"
chrono = { workspace = true, default-features = false, features = ["clock", "std"] }
fast-float.workspace = true
once_cell = { workspace = true, features = ["std"] }
tap = "1.0.1"
@ -93,6 +92,7 @@ bytemuck = { version = "1.14.0", features = ["derive"] }
arrayvec = "0.7.4"
intrusive-collections = "0.9.6"
cfg-if = "1.0.0"
time.workspace = true
# intl deps
boa_icu_provider = {workspace = true, features = ["std"], optional = true }

1559
core/engine/src/builtins/date/mod.rs

File diff suppressed because it is too large Load Diff

143
core/engine/src/builtins/date/tests.rs

@ -1,46 +1,81 @@
use crate::{js_string, run_test_actions, JsNativeErrorKind, TestAction};
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
use indoc::indoc;
// NOTE: Javascript Uses 0-based months, where chrono uses 1-based months. Many of the assertions look wrong because of
// this.
use time::{macros::format_description, util::local_offset, OffsetDateTime};
// NOTE: Javascript Uses 0-based months, where time uses 1-based months.
// Many of the assertions look wrong because of this.
fn month_from_u8(month: u8) -> time::Month {
match month {
1 => time::Month::January,
2 => time::Month::February,
3 => time::Month::March,
4 => time::Month::April,
5 => time::Month::May,
6 => time::Month::June,
7 => time::Month::July,
8 => time::Month::August,
9 => time::Month::September,
10 => time::Month::October,
11 => time::Month::November,
12 => time::Month::December,
_ => unreachable!(),
}
}
fn from_local(
year: i32,
month: u8,
date: u8,
hour: u8,
minute: u8,
second: u8,
millisecond: u16,
) -> OffsetDateTime {
// Safety: This is needed during tests because cargo is running tests in multiple threads.
// It is safe because tests do not modify the environment.
#[cfg(test)]
unsafe {
local_offset::set_soundness(local_offset::Soundness::Unsound);
}
let t = time::Date::from_calendar_date(year, month_from_u8(month), date)
.unwrap()
.with_hms_milli(hour, minute, second, millisecond)
.unwrap()
.assume_utc();
let offset = time::UtcOffset::local_offset_at(t).unwrap();
t.replace_offset(offset)
}
fn timestamp_from_local(
year: i32,
month: u32,
date: u32,
hour: u32,
minute: u32,
second: u32,
millisecond: u32,
month: u8,
date: u8,
hour: u8,
minute: u8,
second: u8,
millisecond: u16,
) -> i64 {
Local
.from_local_datetime(
&NaiveDate::from_ymd_opt(year, month, date)
.unwrap()
.and_hms_milli_opt(hour, minute, second, millisecond)
.unwrap(),
)
.earliest()
.unwrap()
.naive_utc()
.timestamp_millis()
let t = from_local(year, month, date, hour, minute, second, millisecond);
t.unix_timestamp() * 1000 + i64::from(t.millisecond())
}
fn timestamp_from_utc(
year: i32,
month: u32,
date: u32,
hour: u32,
minute: u32,
second: u32,
millisecond: u32,
month: u8,
date: u8,
hour: u8,
minute: u8,
second: u8,
millisecond: u16,
) -> i64 {
NaiveDate::from_ymd_opt(year, month, date)
let t = time::Date::from_calendar_date(year, month_from_u8(month), date)
.unwrap()
.and_hms_milli_opt(hour, minute, second, millisecond)
.with_hms_milli(hour, minute, second, millisecond)
.unwrap()
.timestamp_millis()
.assume_utc();
t.unix_timestamp() * 1000 + i64::from(t.millisecond())
}
#[test]
@ -257,18 +292,8 @@ fn date_proto_get_timezone_offset() {
TestAction::assert_eq(
"new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset()",
{
// The value of now().offset() depends on the host machine, so we have to replicate the method code here.
let dt = Local
.from_local_datetime(
&NaiveDate::from_ymd_opt(1975, 8, 19)
.unwrap()
.and_hms_opt(23, 15, 30)
.unwrap(),
)
.earliest()
.unwrap();
let offset_seconds = dt.offset().local_minus_utc();
-offset_seconds / 60
let t = from_local(1975, 8, 19, 23, 15, 30, 0);
-t.offset().whole_seconds() / 60
},
),
]);
@ -792,33 +817,31 @@ fn date_proto_to_json() {
#[test]
fn date_proto_to_string() {
let to_string_format = format_description!(
"[weekday repr:short] [month repr:short] [day] [year] [hour]:[minute]:[second] GMT[offset_hour sign:mandatory][offset_minute][end]"
);
let t = from_local(2020, 7, 8, 9, 16, 15, 779)
.format(to_string_format)
.unwrap();
run_test_actions([TestAction::assert_eq(
"new Date(2020, 6, 8, 9, 16, 15, 779).toString()",
js_string!(Local
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 7, 8).unwrap(),
NaiveTime::from_hms_milli_opt(9, 16, 15, 779).unwrap(),
))
.earliest()
.unwrap()
.format("Wed Jul 08 2020 09:16:15 GMT%z")
.to_string()),
js_string!(t),
)]);
}
#[test]
fn date_proto_to_time_string() {
let to_time_string_format = format_description!(
"[hour]:[minute]:[second] GMT[offset_hour sign:mandatory][offset_minute][end]"
);
let t = from_local(2020, 7, 8, 9, 16, 15, 779)
.format(to_time_string_format)
.unwrap();
run_test_actions([TestAction::assert_eq(
"new Date(2020, 6, 8, 9, 16, 15, 779).toTimeString()",
js_string!(Local
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 7, 8).unwrap(),
NaiveTime::from_hms_milli_opt(9, 16, 15, 779).unwrap(),
))
.earliest()
.unwrap()
.format("09:16:15 GMT%z")
.to_string()),
js_string!(t),
)]);
}

946
core/engine/src/builtins/date/utils.rs

File diff suppressed because it is too large Load Diff

58
core/engine/src/context/hooks.rs

@ -1,13 +1,15 @@
use crate::{
builtins::promise::OperationType,
context::intrinsics::Intrinsics,
job::JobCallback,
object::{JsFunction, JsObject},
realm::Realm,
Context, JsResult, JsValue,
};
use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone, Utc};
use time::{OffsetDateTime, UtcOffset};
use super::intrinsics::Intrinsics;
#[cfg(test)]
use time::util::local_offset;
/// [`Host Hooks`] customizable by the host code or engine.
///
@ -170,46 +172,28 @@ pub trait HostHooks {
/// Gets the current UTC time of the host.
///
/// Defaults to using [`Utc::now`] on all targets, which can cause panics if the target
/// doesn't support [`SystemTime::now`][time].
/// Defaults to using [`OffsetDateTime::now_utc`] on all targets,
/// which can cause panics if the target doesn't support [`SystemTime::now`][time].
///
/// [time]: std::time::SystemTime::now
fn utc_now(&self) -> NaiveDateTime {
Utc::now().naive_utc()
fn utc_now(&self) -> i64 {
let now = OffsetDateTime::now_utc();
now.unix_timestamp() * 1000 + i64::from(now.millisecond())
}
/// Converts the naive datetime `utc` to the corresponding local datetime.
///
/// Defaults to using [`Local`] on all targets, which can cause panics if the taget
/// doesn't support [`SystemTime::now`][time].
///
/// [time]: std::time::SystemTime::now
fn local_from_utc(&self, utc: NaiveDateTime) -> DateTime<FixedOffset> {
let offset = Local.offset_from_utc_datetime(&utc);
offset.from_utc_datetime(&utc)
}
/// Converts the naive local datetime `local` to a local timezone datetime.
///
/// Defaults to using [`Local`] on all targets, which can cause panics if the target
/// doesn't support [`SystemTime::now`][time].
///
/// [time]: std::time::SystemTime::now
fn local_from_naive_local(&self, local: NaiveDateTime) -> LocalResult<DateTime<FixedOffset>> {
match Local.offset_from_local_datetime(&local) {
LocalResult::None => LocalResult::None,
LocalResult::Single(offset) => offset.from_local_datetime(&local),
LocalResult::Ambiguous(earliest, latest) => {
match (
earliest.from_local_datetime(&local).earliest(),
latest.from_local_datetime(&local).latest(),
) {
(Some(earliest), Some(latest)) => LocalResult::Ambiguous(earliest, latest),
(Some(dt), None) | (None, Some(dt)) => LocalResult::Single(dt),
(None, None) => LocalResult::None,
}
}
/// Returns the offset of the local timezone to the `utc` timezone in seconds.
fn local_timezone_offset_seconds(&self, unix_time_seconds: i64) -> i32 {
// Safety: This is needed during tests because cargo is running tests in multiple threads.
// It is safe because tests do not modify the environment.
#[cfg(test)]
unsafe {
local_offset::set_soundness(local_offset::Soundness::Unsound);
}
OffsetDateTime::from_unix_timestamp(unix_time_seconds)
.ok()
.and_then(|t| UtcOffset::local_offset_at(t).ok())
.map_or(0, UtcOffset::whole_seconds)
}
/// Gets the maximum size in bits that can be allocated for an `ArrayBuffer` or a

11
core/engine/src/object/builtins/jsdate.rs

@ -1,8 +1,4 @@
//! A Rust API wrapper for Boa's `Date` ECMAScript Builtin Object.
use std::ops::Deref;
use boa_gc::{Finalize, Trace};
use chrono::DateTime;
use crate::{
builtins::Date,
@ -10,6 +6,9 @@ use crate::{
value::TryFromJs,
Context, JsNativeError, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use std::ops::Deref;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
/// `JsDate` is a wrapper for JavaScript `JsDate` builtin object
///
@ -551,9 +550,9 @@ impl JsDate {
.to_string(context)?
.to_std_string()
.map_err(|_| JsNativeError::typ().with_message("unpaired surrogate on date string"))?;
let date_time = DateTime::parse_from_rfc3339(&string)
let t = OffsetDateTime::parse(&string, &Rfc3339)
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?;
let date_time = Date::new(Some(date_time.naive_local().timestamp_millis()));
let date_time = Date::new((t.unix_timestamp() * 1000 + i64::from(t.millisecond())) as f64);
Ok(Self {
inner: JsObject::from_proto_and_data_with_shared_shape(

23
core/engine/src/value/integer.rs

@ -99,26 +99,3 @@ impl PartialOrd<IntegerOrInfinity> for i64 {
}
}
}
/// Represents the result of the `to_integer_or_nan` method.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum IntegerOrNan {
Integer(i64),
Nan,
}
impl IntegerOrNan {
/// Gets the wrapped `i64` if the variant is an `Integer`.
pub(crate) const fn as_integer(self) -> Option<i64> {
match self {
Self::Integer(i) => Some(i),
Self::Nan => None,
}
}
}
impl From<IntegerOrInfinity> for IntegerOrNan {
fn from(ior: IntegerOrInfinity) -> Self {
ior.as_integer().map_or(Self::Nan, IntegerOrNan::Integer)
}
}

18
core/engine/src/value/mod.rs

@ -45,7 +45,7 @@ pub use self::{
#[doc(inline)]
pub use boa_macros::TryFromJs;
pub(crate) use self::{conversions::IntoOrUndefined, integer::IntegerOrNan};
pub(crate) use self::conversions::IntoOrUndefined;
static TWO_E_64: Lazy<BigInt> = Lazy::new(|| {
const TWO_E_64: u128 = 2u128.pow(64);
@ -855,22 +855,6 @@ impl JsValue {
Ok(IntegerOrInfinity::from(number))
}
/// Modified abstract operation `ToIntegerOrInfinity ( argument )`.
///
/// This function is almost the same as [`Self::to_integer_or_infinity`], but with the exception
/// that this will return `Nan` if [`Self::to_number`] returns a non-finite number.
pub(crate) fn to_integer_or_nan(&self, context: &mut Context) -> JsResult<IntegerOrNan> {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;
if number.is_nan() {
return Ok(IntegerOrNan::Nan);
}
// Continues on `IntegerOrInfinity::from::<f64>`
Ok(IntegerOrInfinity::from(number).into())
}
/// Converts a value to a double precision floating point.
///
/// This function is equivalent to the unary `+` operator (`+value`) in JavaScript

2
examples/Cargo.toml

@ -16,7 +16,7 @@ boa_interner.workspace = true
boa_gc.workspace = true
boa_parser.workspace = true
boa_runtime.workspace = true
chrono.workspace = true
time.workspace = true
smol = "2.0.0"
futures-util = "0.3.30"

15
examples/src/bin/jsdate.rs

@ -1,22 +1,15 @@
use boa_engine::{
context::HostHooks, js_string, object::builtins::JsDate, Context, JsResult, JsValue,
};
use chrono::{DateTime, FixedOffset, LocalResult, NaiveDateTime, TimeZone};
struct CustomTimezone;
// This pins the local timezone to a system-agnostic value; in this case, UTC+3
impl HostHooks for CustomTimezone {
fn local_from_utc(&self, utc: NaiveDateTime) -> DateTime<FixedOffset> {
FixedOffset::east_opt(3 * 3600)
.unwrap()
.from_utc_datetime(&utc)
}
fn local_from_naive_local(&self, local: NaiveDateTime) -> LocalResult<DateTime<FixedOffset>> {
FixedOffset::east_opt(3 * 3600)
.unwrap()
.from_local_datetime(&local)
fn local_timezone_offset_seconds(&self, _: i64) -> i32 {
time::UtcOffset::from_hms(3, 0, 0)
.expect("must be valid offset")
.whole_seconds()
}
}

1
ffi/wasm/Cargo.toml

@ -15,7 +15,6 @@ rust-version.workspace = true
boa_engine = { workspace = true, features = ["js"] }
wasm-bindgen = { version = "0.2.90", default-features = false }
getrandom = { version = "0.2.12", features = ["js"] }
chrono = { workspace = true, default-features = false, features = ["clock", "std", "wasmbind"] }
console_error_panic_hook = "0.1.7"
[features]

1
ffi/wasm/src/lib.rs

@ -7,7 +7,6 @@
#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
use boa_engine::{Context, Source};
use chrono as _;
use getrandom as _;
use wasm_bindgen::prelude::*;

1
tests/tester/Cargo.toml

@ -31,6 +31,7 @@ phf = { workspace = true, features = ["macros"] }
comfy-table = "7.1.0"
serde_repr = "0.1.18"
bus = "2.4.1"
time.workspace = true
[features]
default = ["boa_engine/intl", "boa_engine/experimental", "boa_engine/annex-b"]

6
tests/tester/src/main.rs

@ -192,6 +192,12 @@ const DEFAULT_TEST262_DIRECTORY: &str = "test262";
/// Program entry point.
fn main() -> Result<()> {
// Safety: This is needed because we run tests in multiple threads.
// It is safe because tests do not modify the environment.
unsafe {
time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound);
}
// initializes the monotonic clock.
Lazy::force(&START);
color_eyre::install()?;

Loading…
Cancel
Save