diff --git a/boa_ast/src/lib.rs b/boa_ast/src/lib.rs index 2451f5697e8..cd08b665d7b 100644 --- a/boa_ast/src/lib.rs +++ b/boa_ast/src/lib.rs @@ -106,8 +106,6 @@ pub use self::{ statement::Statement, statement_list::{StatementList, StatementListItem}, }; -#[cfg(feature = "temporal")] -pub use temporal::UtcOffset; /// Utility to join multiple Nodes into a single string. fn join_nodes(interner: &Interner, nodes: &[N]) -> String diff --git a/boa_ast/src/temporal.rs b/boa_ast/src/temporal.rs deleted file mode 100644 index 3889dbfc3fe..00000000000 --- a/boa_ast/src/temporal.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! AST nodes for Temporal. - -#[derive(Debug, Clone, Copy)] -#[allow(dead_code, missing_docs)] -pub enum OffsetSign { - Positive, - Negative, -} - -/// UTC offset information. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// -/// [spec]: https://tc39.es/ecma262/#prod-UTCOffset -#[derive(Debug, Clone)] -#[allow(dead_code, missing_docs)] -pub struct UtcOffset { - pub sign: OffsetSign, - pub hour: String, - pub minute: Option, - pub second: Option, - pub fraction: Option, -} diff --git a/boa_ast/src/temporal/annotation.rs b/boa_ast/src/temporal/annotation.rs new file mode 100644 index 00000000000..695ab07e4cc --- /dev/null +++ b/boa_ast/src/temporal/annotation.rs @@ -0,0 +1,13 @@ +//! Temporal Annotation Nodes. + +/// A list of indidividual Annotation nodes. +#[derive(Debug, Clone, Copy)] +#[deny(dead_code)] +pub struct Annotations; +//{ +// list: Box<[Annotation]>, +//} + +/// An individual Annotation Information Node +#[derive(Debug, Clone, Copy)] +pub struct Annotation; diff --git a/boa_ast/src/temporal/mod.rs b/boa_ast/src/temporal/mod.rs new file mode 100644 index 00000000000..c2f4c84a21c --- /dev/null +++ b/boa_ast/src/temporal/mod.rs @@ -0,0 +1,113 @@ +//! AST nodes for Temporal's implementation of ISO8601 grammar. + +use rustc_hash::FxHashMap; + +pub mod annotation; + +/// TBD... +#[derive(Default, Debug)] +pub struct AnnotatedDateTime { + /// Parsed Date Record + pub date_time: DateTimeRecord, + /// Parsed `TimeZoneAnnotation` + pub tz_annotation: Option, + /// Parsed Annotations + pub annotations: Option>, +} + +#[derive(Default, Debug, Clone,Copy)] +/// The record of a parsed date. +pub struct DateRecord { + /// Date Year + pub year: i32, + /// Date Month + pub month: i32, + /// Date Day + pub day: i32, +} + +/// Parsed Time info +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct TimeSpec { + /// An hour + pub hour: i8, + /// A minute value + pub minute: i8, + /// A floating point second value. + pub second: f64, +} + +/// TimeZone UTC Offset info. +#[derive(Debug, Clone, Copy)] +pub struct DateTimeUtcOffset; + +#[derive(Debug, Default, Clone, Copy)] +/// Boop +pub struct DateTimeRecord { + /// Date + pub date: DateRecord, + /// Time + pub time: Option, + /// Tz Offset + pub offset: Option, +} + +/// A TimeZoneAnnotation. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct TimeZoneAnnotation { + /// Critical Flag for the annotation. + pub critical: bool, + /// TimeZone Data + pub tz: TzIdentifier, +} + +/// A valid `TimeZoneIdentifier` that is defined by +/// the specification as either a UTC Offset to minute +/// precision or a `TimeZoneIANAName` +#[derive(Debug, Clone)] +pub enum TzIdentifier { + /// A valid UTC `TimeZoneIdentifier` value + UtcOffset(UtcOffset), + /// A valid IANA name `TimeZoneIdentifier` value + TzIANAName(String), +} + +// NOTE: is it worth consolidating MinutePrecision vs. Offset +/// A UTC Offset that maintains only minute precision. +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct UtcOffsetMinutePrecision { + sign: i8, + hour: i8, + minute: i8, +} + +/// A full precision `UtcOffset` +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct UtcOffset { + /// The UTC flag + pub utc: bool, + /// The `+`/`-` sign of this `UtcOffset` + pub sign: i8, + /// The hour value of the `UtcOffset` + pub hour: i8, + /// The minute value of the `UtcOffset`. + pub minute: i8, + /// A float representing the second value of the `UtcOffset`. + pub second: f64, +} + +/// A KeyValueAnnotation Parse Node. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct KeyValueAnnotation { + /// An `Annotation`'s Key. + pub key: String, + /// An `Annotation`'s value. + pub value: String, + /// Whether the annotation was flagged as critical. + pub critical: bool, +} \ No newline at end of file diff --git a/boa_engine/src/builtins/temporal/calendar/mod.rs b/boa_engine/src/builtins/temporal/calendar/mod.rs index 1336a188873..019c73e248c 100644 --- a/boa_engine/src/builtins/temporal/calendar/mod.rs +++ b/boa_engine/src/builtins/temporal/calendar/mod.rs @@ -981,41 +981,65 @@ pub(crate) fn calendar_date_until( // b. Return ? Call(%Temporal.Calendar.prototype.dateUntil%, calendar, « one, two, options »). // 2. If dateUntil is not present, set dateUntil to ? GetMethod(calendar, "dateUntil"). // 3. Let duration be ? Call(dateUntil, calendar, « one, two, options »). - let duration = call_method_on_abstract_calendar(calendar, &JsString::from("dateUntil"), &[one.clone().into(), two.clone().into(), options.clone()], context)?; + let duration = call_method_on_abstract_calendar( + calendar, + &JsString::from("dateUntil"), + &[one.clone().into(), two.clone().into(), options.clone()], + context, + )?; // 4. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). // 5. Return duration. match duration { - JsValue::Object(o) if o.is_duration()=>{ + JsValue::Object(o) if o.is_duration() => { let obj = o.borrow(); - let dur = obj.as_duration().expect("Value is confirmed to be a duration."); + let dur = obj + .as_duration() + .expect("Value is confirmed to be a duration."); let record = dur.inner; drop(obj); - return Ok(record) - }, - _=> Err(JsNativeError::typ().with_message("Calendar dateUntil must return a Duration").into()) + return Ok(record); + } + _ => Err(JsNativeError::typ() + .with_message("Calendar dateUntil must return a Duration") + .into()), } } /// 12.2.6 CalendarYear ( calendar, dateLike ) /// /// Returns either a normal completion containing an integer, or an abrupt completion. -pub(crate) fn calendar_year(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.year%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "year", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("year"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("year"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarYear was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarYear was not integral.") + .into()); } // 5. Return ℝ(result). @@ -1023,27 +1047,44 @@ pub(crate) fn calendar_year(calendar: &JsValue, datelike: &JsValue, context: &mu } /// 12.2.7 CalendarMonth ( calendar, dateLike ) -pub(crate) fn calendar_month(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_month( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.month%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "month", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("month"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("month"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarMonth was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarMonth was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("month must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("month must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1051,43 +1092,71 @@ pub(crate) fn calendar_month(calendar: &JsValue, datelike: &JsValue, context: &m } /// 12.2.8 CalendarMonthCode ( calendar, dateLike ) -pub(crate) fn calendar_month_code(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_month_code( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.monthCode%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "monthCode", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("monthCode"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("monthCode"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not String, throw a TypeError exception. // 4. Return result. match result { - JsValue::String(s)=> Ok(s), - _=> Err(JsNativeError::typ().with_message("monthCode must be a String.").into()) + JsValue::String(s) => Ok(s), + _ => Err(JsNativeError::typ() + .with_message("monthCode must be a String.") + .into()), } } /// 12.2.9 CalendarDay ( calendar, dateLike ) -pub(crate) fn calendar_day(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_day( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.day%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "day", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("day"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("day"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarDay was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarDay was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("day must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("day must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1095,27 +1164,44 @@ pub(crate) fn calendar_day(calendar: &JsValue, datelike: &JsValue, context: &mut } /// 12.2.10 CalendarDayOfWeek ( calendar, dateLike ) -pub(crate) fn calendar_day_of_week(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_day_of_week( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.dayOfWeek%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "dayOfWeek", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("dayOfWeek"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("dayOfWeek"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarDayOfWeek result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarDayOfWeek result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarDayOfWeek was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarDayOfWeek was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("dayOfWeek must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("dayOfWeek must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1123,27 +1209,44 @@ pub(crate) fn calendar_day_of_week(calendar: &JsValue, datelike: &JsValue, conte } /// 12.2.11 CalendarDayOfYear ( calendar, dateLike ) -pub(crate) fn calendar_day_of_year(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_day_of_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.dayOfYear%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "dayOfYear", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("dayOfYear"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("dayOfYear"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarDayOfYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarDayOfYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarDayOfYear was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarDayOfYear was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("dayOfYear must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("dayOfYear must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1151,27 +1254,44 @@ pub(crate) fn calendar_day_of_year(calendar: &JsValue, datelike: &JsValue, conte } /// 12.2.12 CalendarWeekOfYear ( calendar, dateLike ) -pub(crate) fn calendar_week_of_year(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_week_of_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.weekOfYear%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "weekOfYear", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("weekOfYear"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("weekOfYear"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarWeekOfYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarWeekOfYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarWeekOfYear was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarWeekOfYear was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("weekOfYear must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("weekOfYear must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1179,22 +1299,37 @@ pub(crate) fn calendar_week_of_year(calendar: &JsValue, datelike: &JsValue, cont } /// 12.2.13 CalendarYearOfWeek ( calendar, dateLike ) -pub(crate) fn calendar_year_of_week(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_year_of_week( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.yearOfWeek%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "yearOfWeek", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("yearOfWeek"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("yearOfWeek"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarYearOfWeek result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarYearOfWeek result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarYearOfWeek was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarYearOfWeek was not integral.") + .into()); } // 5. Return ℝ(result). @@ -1202,27 +1337,44 @@ pub(crate) fn calendar_year_of_week(calendar: &JsValue, datelike: &JsValue, cont } /// 12.2.14 CalendarDaysInWeek ( calendar, dateLike ) -pub(crate) fn calendar_days_in_week(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_days_in_week( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.daysInWeek%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "daysInWeek", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("daysInWeek"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("daysInWeek"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarDaysInWeek result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarDaysInWeek result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarDaysInWeek was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarDaysInWeek was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("daysInWeek must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("daysInWeek must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1230,27 +1382,44 @@ pub(crate) fn calendar_days_in_week(calendar: &JsValue, datelike: &JsValue, cont } /// 12.2.15 CalendarDaysInMonth ( calendar, dateLike ) -pub(crate) fn calendar_days_in_month(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_days_in_month( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.daysInMonth%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "daysInMonth", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("daysInMonth"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("daysInMonth"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarDaysInMonth result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarDaysInMonth result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarDaysInMonth was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarDaysInMonth was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("daysInMonth must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("daysInMonth must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1258,27 +1427,44 @@ pub(crate) fn calendar_days_in_month(calendar: &JsValue, datelike: &JsValue, con } /// 12.2.16 CalendarDaysInYear ( calendar, dateLike ) -pub(crate) fn calendar_days_in_year(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_days_in_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.daysInYear%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "daysInYear", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("daysInYear"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("daysInYear"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarDaysInYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarDaysInYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarDaysInYear was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarDaysInYear was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("daysInYear must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("daysInYear must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1286,27 +1472,44 @@ pub(crate) fn calendar_days_in_year(calendar: &JsValue, datelike: &JsValue, cont } /// 12.2.17 CalendarMonthsInYear ( calendar, dateLike ) -pub(crate) fn calendar_months_in_year(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_months_in_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.monthsInYear%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "monthsInYear", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("monthsInYear"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("monthsInYear"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Number, throw a TypeError exception. let number = match result.as_number() { - Some(n)=> n, - None=>return Err(JsNativeError::typ().with_message("CalendarMonthsInYear result must be a number.").into()) + Some(n) => n, + None => { + return Err(JsNativeError::typ() + .with_message("CalendarMonthsInYear result must be a number.") + .into()) + } }; // 4. If IsIntegralNumber(result) is false, throw a RangeError exception. if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { - return Err(JsNativeError::range().with_message("CalendarMonthsInYear was not integral.").into()) + return Err(JsNativeError::range() + .with_message("CalendarMonthsInYear was not integral.") + .into()); } // 5. If result < 1𝔽, throw a RangeError exception. if number < 1.0 { - return Err(JsNativeError::range().with_message("monthsInYear must be 1 or greater.").into()) + return Err(JsNativeError::range() + .with_message("monthsInYear must be 1 or greater.") + .into()); } // 6. Return ℝ(result). @@ -1314,18 +1517,29 @@ pub(crate) fn calendar_months_in_year(calendar: &JsValue, datelike: &JsValue, co } /// 12.2.18 CalendarInLeapYear ( calendar, dateLike ) -pub(crate) fn calendar_in_lear_year(calendar: &JsValue, datelike: &JsValue, context: &mut Context<'_>) -> JsResult { +pub(crate) fn calendar_in_lear_year( + calendar: &JsValue, + datelike: &JsValue, + context: &mut Context<'_>, +) -> JsResult { // 1. If calendar is a String, then // a. Set calendar to ! CreateTemporalCalendar(calendar). // b. Return ? Call(%Temporal.Calendar.prototype.inLeapYear%, calendar, « dateLike »). // 2. Let result be ? Invoke(calendar, "inLeapYear", « dateLike »). - let result = call_method_on_abstract_calendar(calendar, &JsString::from("inLeapYear"), &[datelike.clone()], context)?; + let result = call_method_on_abstract_calendar( + calendar, + &JsString::from("inLeapYear"), + &[datelike.clone()], + context, + )?; // 3. If Type(result) is not Boolean, throw a TypeError exception. // 4. Return result. match result { - JsValue::Boolean(b)=> Ok(b), - _=> Err(JsNativeError::typ().with_message("inLeapYear result must be a boolean.").into()) + JsValue::Boolean(b) => Ok(b), + _ => Err(JsNativeError::typ() + .with_message("inLeapYear result must be a boolean.") + .into()), } } diff --git a/boa_engine/src/builtins/temporal/duration/mod.rs b/boa_engine/src/builtins/temporal/duration/mod.rs index b517764bb76..65161b2a76c 100644 --- a/boa_engine/src/builtins/temporal/duration/mod.rs +++ b/boa_engine/src/builtins/temporal/duration/mod.rs @@ -900,7 +900,9 @@ pub(crate) fn to_temporal_duration(item: &JsValue, context: &mut Context<'_>) -> pub(crate) fn to_temporal_duration_record( _temporal_duration_like: &JsValue, ) -> JsResult { - Err(JsNativeError::range().with_message("Not yet implemented.").into()) + Err(JsNativeError::range() + .with_message("Not yet implemented.") + .into()) } /// 7.5.14 `CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )` diff --git a/boa_engine/src/builtins/temporal/mod.rs b/boa_engine/src/builtins/temporal/mod.rs index 288327f37c8..99a88c44277 100644 --- a/boa_engine/src/builtins/temporal/mod.rs +++ b/boa_engine/src/builtins/temporal/mod.rs @@ -39,7 +39,7 @@ use crate::{ Context, JsBigInt, JsNativeError, JsNativeErrorKind, JsObject, JsResult, JsString, JsSymbol, JsValue, NativeFunction, }; -use boa_ast::temporal::{self, OffsetSign, UtcOffset}; +use boa_ast::temporal::{self, UtcOffset}; use boa_profiler::Profiler; // Relavant numeric constants diff --git a/boa_engine/src/builtins/temporal/plain_date/mod.rs b/boa_engine/src/builtins/temporal/plain_date/mod.rs index 21b11162538..9a15b381776 100644 --- a/boa_engine/src/builtins/temporal/plain_date/mod.rs +++ b/boa_engine/src/builtins/temporal/plain_date/mod.rs @@ -580,7 +580,7 @@ pub(crate) fn to_temporal_date( // 5. If item is not a String, throw a TypeError exception. match item { - JsValue::String(s)=> { + JsValue::String(s) => { // 6. Let result be ? ParseTemporalDateString(item). // 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true. // 8. Let calendar be result.[[Calendar]]. @@ -593,13 +593,9 @@ pub(crate) fn to_temporal_date( Err(JsNativeError::range() .with_message("Not yet implemented.") .into()) - }, - _=> { - Err(JsNativeError::typ() - .with_message("ToTemporalDate item must be an object or string.") - .into()) - }, + } + _ => Err(JsNativeError::typ() + .with_message("ToTemporalDate item must be an object or string.") + .into()), } - - } diff --git a/boa_engine/src/builtins/temporal/time_zone/mod.rs b/boa_engine/src/builtins/temporal/time_zone/mod.rs index e0378074382..6dbcddb32ec 100644 --- a/boa_engine/src/builtins/temporal/time_zone/mod.rs +++ b/boa_engine/src/builtins/temporal/time_zone/mod.rs @@ -1,7 +1,6 @@ #![allow(dead_code)] -use boa_ast::{temporal::OffsetSign, UtcOffset}; -use boa_parser::parser::UTCOffset; +use boa_ast::temporal::{UtcOffset, TzIdentifier}; use crate::{ builtins::{ @@ -334,36 +333,31 @@ pub(super) fn create_temporal_time_zone( /// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring #[allow(clippy::unnecessary_wraps, unused)] fn parse_timezone_offset_string(offset_string: &str, context: &mut Context<'_>) -> JsResult { - use boa_parser::parser::{Cursor, TokenParser}; + use boa_parser::temporal::{IsoCursor, TemporalTimeZoneString}; // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset). - let parse_result = UTCOffset - .parse( - &mut Cursor::new(offset_string.as_bytes()), - context.interner_mut(), - ) - // 2. Assert: parseResult is not a List of errors. - .expect("must not fail as per the spec"); + let parse_result = TemporalTimeZoneString::parse(&mut IsoCursor::new(offset_string.to_string()))?; + // 2. Assert: parseResult is not a List of errors. // 3. Assert: parseResult contains a TemporalSign Parse Node. + let utc_offset = match parse_result { + TzIdentifier::UtcOffset(utc)=>utc, + _=> return Err(JsNativeError::typ().with_message("Offset string was not a valid offset").into()) + }; // 4. Let parsedSign be the source text matched by the TemporalSign Parse Node contained within // parseResult. // 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS) or U+2212 (MINUS SIGN), then - let sign = if matches!(parse_result.sign, OffsetSign::Negative) { + let sign = utc_offset.sign; // a. Let sign be -1. - -1 - } else { // 6. Else, // a. Let sign be 1. - 1 - }; // 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed // values is guaranteed to be a sufficiently short string of decimal digits. // 8. Assert: parseResult contains an Hour Parse Node. // 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult. - let parsed_hours = parse_result.hour; + let parsed_hours = utc_offset.hour; // 10. Let hours be ℝ(StringToNumber(CodePointsToString(parsedHours))). // 11. If parseResult does not contain a MinuteSecond Parse Node, then diff --git a/boa_parser/src/lib.rs b/boa_parser/src/lib.rs index b26d72c4861..706f23fc8a5 100644 --- a/boa_parser/src/lib.rs +++ b/boa_parser/src/lib.rs @@ -79,6 +79,8 @@ pub mod error; pub mod lexer; pub mod parser; mod source; +#[cfg(feature = "temporal")] +pub mod temporal; pub use error::{Error, ParseResult}; pub use lexer::Lexer; diff --git a/boa_parser/src/parser/mod.rs b/boa_parser/src/parser/mod.rs index 0ebfa8229c1..219ca914221 100644 --- a/boa_parser/src/parser/mod.rs +++ b/boa_parser/src/parser/mod.rs @@ -3,8 +3,6 @@ mod cursor; mod expression; mod statement; -#[cfg(feature = "temporal")] -pub mod temporal; pub(crate) mod function; @@ -33,8 +31,6 @@ use std::{io::Read, path::Path}; use self::statement::ModuleItemList; pub use self::cursor::Cursor; -#[cfg(feature = "temporal")] -pub use self::temporal::UTCOffset; /// Trait implemented by parsers. /// diff --git a/boa_parser/src/parser/temporal/mod.rs b/boa_parser/src/parser/temporal/mod.rs deleted file mode 100644 index 7eafbb299d6..00000000000 --- a/boa_parser/src/parser/temporal/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Parsers for `Temporal` syntax. - -#![allow(unused_variables)] // Unimplemented - -use crate::ParseResult; - -use super::{cursor::Cursor, TokenParser}; -use boa_interner::Interner; -use std::io::Read; - -/// `TimeZoneNumericUTCOffset` parser. -/// -/// More information: -/// - [ECMAScript specification][spec] -/// -/// [spec]: https://tc39.es/proposal-temporal/#prod-TimeZoneNumericUTCOffset -#[derive(Debug, Clone, Copy)] -pub struct UTCOffset; - -impl TokenParser for UTCOffset -where - R: Read, -{ - type Output = boa_ast::UtcOffset; - - fn parse(self, cursor: &mut Cursor, interner: &mut Interner) -> ParseResult { - todo!() - } -} diff --git a/boa_parser/src/temporal/annotations.rs b/boa_parser/src/temporal/annotations.rs new file mode 100644 index 00000000000..385025e5713 --- /dev/null +++ b/boa_parser/src/temporal/annotations.rs @@ -0,0 +1,130 @@ +/// Parsing for Temporal's `Annotations`. + +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, + temporal::{IsoCursor, grammar::*} +}; + +use boa_ast::{ + Position, + temporal::KeyValueAnnotation, +}; + +use rustc_hash::FxHashMap; + +/// Parse any number of `KeyValueAnnotation`s +pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult> { + let mut hash_map = FxHashMap::default(); + while let Some(annotation_open) = cursor.peek() { + if *annotation_open == '[' { + let kv = parse_kv_annotation(cursor)?; + if !hash_map.contains_key(&kv.key) { + hash_map.insert(kv.key, (kv.critical, kv.value)); + } + } else { + break; + } + } + + return Ok(hash_map); +} + +/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair. +fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult { + assert!(*cursor.peek().unwrap() == '['); + // TODO: remove below if unneeded. + let _start = Position::new(1, (cursor.pos() + 1) as u32); + + let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; + let (leading_char, critical) = if *potential_critical == '!' { + (cursor.next().ok_or_else(|| Error::AbruptEnd)?, true) + } else { + (potential_critical, false) + }; + + if !is_a_key_leading_char(leading_char) { + return Err(LexError::syntax( + "Invalid AnnotationKey leading character", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + + // Parse AnnotationKey. + let annotation_key = parse_annotation_key(cursor)?; + + // Advance past the '=' character. + assert!(*cursor.peek().unwrap() == '='); + cursor.advance(); + + // Parse AnnotationValue. + let annotation_value = parse_annotation_value(cursor)?; + + // Assert that we are at the annotation close and advance cursor past annotation to close. + assert!(*cursor.peek().unwrap() == ']'); + // TODO: remove below if unneeded. + let _end = Position::new(1, (cursor.pos() + 1) as u32); + cursor.advance(); + + return Ok(KeyValueAnnotation { + key: annotation_key, + value: annotation_value, + critical, + }); +} + +/// Parse an `AnnotationKey`. +fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult { + let key_start = cursor.pos(); + while let Some(potential_key_char) = cursor.next() { + // End of key. + if *potential_key_char == '=' { + // Return found key + return Ok(cursor.slice(key_start, cursor.pos())); + } + + if !is_a_key_char(potential_key_char) { + return Err(LexError::syntax( + "Invalid AnnotationKey Character", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + } + + Err(Error::AbruptEnd) +} + +/// Parse an `AnnotationValue`. +fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult { + let value_start = cursor.pos(); + while let Some(potential_value_char) = cursor.next() { + if *potential_value_char == ']' { + // Return the determined AnnotationValue. + return Ok(cursor.slice(value_start, cursor.pos())); + } + + if *potential_value_char == '-' { + if !cursor + .peek_n(1) + .map(|ch| is_annotation_value_component(ch)) + .unwrap_or(false) + { + return Err(LexError::syntax( + "Missing AttributeValueComponent after '-'", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance(); + continue; + } + + if !is_annotation_value_component(potential_value_char) { + return Err(LexError::syntax( + "Invalid character in AnnotationValue", + Position::new(1, (value_start + cursor.pos() + 1) as u32), + ).into()); + } + } + + Err(Error::AbruptEnd) +} \ No newline at end of file diff --git a/boa_parser/src/temporal/date_time.rs b/boa_parser/src/temporal/date_time.rs new file mode 100644 index 00000000000..fee11442696 --- /dev/null +++ b/boa_parser/src/temporal/date_time.rs @@ -0,0 +1,220 @@ +//! Parsing for Temporal's ISO8601 `Date` and `DateTime`. + +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, + temporal::{ + IsoCursor, + grammar::*, + time_zone, + time, + annotations, + } +}; + +use boa_ast::{ + Position, Span, + temporal::{AnnotatedDateTime, DateRecord,DateTimeRecord} +}; + +/// `AnnotatedDateTime` +/// +/// Defined in Temporal Proposal as follows: +/// +/// AnnotatedDateTime[Zoned] : +/// [~Zoned] DateTime TimeZoneAnnotation(opt) Annotations(opt) +/// [+Zoned] DateTime TimeZoneAnnotation Annotations(opt) +pub(crate) fn parse_annotated_date_time( + zoned: bool, + cursor: &mut IsoCursor, +) -> ParseResult { + let date_time = parse_date_time(cursor)?; + + // Peek Annotation presence + // Throw error if annotation does not exist and zoned is true, else return. + let annotation_check = cursor.peek().map(|ch| *ch == '[').unwrap_or(false); + if !annotation_check { + if zoned { + return Err(Error::expected( + ["TimeZoneAnnotation".into()], + "No Annotation", + Span::new( + Position::new(1, (cursor.pos() + 1) as u32), + Position::new(1, (cursor.pos() + 1) as u32), + ), + "iso8601 grammar", + )); + } + return Ok(AnnotatedDateTime { date_time, tz_annotation: None, annotations: None }); + } + + // Parse the first annotation. + let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?; + + if tz_annotation.is_none() && zoned { + return Err(Error::unexpected( + "Annotation", + Span::new(Position::new(1, (cursor.pos() + 1) as u32), Position::new(1, (cursor.pos() + 2) as u32)), + "iso8601 ZonedDateTime requires a TimeZoneAnnotation.", + )); + } + + // Parse any `Annotations` + let annotations = cursor.peek().map(|ch| *ch == '[').unwrap_or(false); + + if annotations { + let annotations = annotations::parse_annotations(cursor)?; + return Ok(AnnotatedDateTime { date_time, tz_annotation, annotations: Some(annotations) }) + } + + Ok(AnnotatedDateTime { date_time, tz_annotation, annotations: None }) +} + +fn parse_date_time(cursor: &mut IsoCursor) -> ParseResult { + let date = parse_date(cursor)?; + + // If there is no `DateTimeSeparator`, return date early. + if !cursor + .peek() + .map(|c| is_date_time_separator(c)) + .unwrap_or(false) + { + return Ok(DateTimeRecord { + date, + time: None, + offset: None, + }); + } + + cursor.advance(); + + let time = time::parse_time_spec(cursor)?; + + let offset = if cursor + .peek() + .map(|ch| is_sign(ch) || is_utc_designator(ch)) + .unwrap_or(false) + { + Some(time_zone::parse_date_time_utc(cursor)?) + } else { + None + }; + + Ok(DateTimeRecord { date, time: Some(time), offset }) +} + + +/// Parse `Date` +fn parse_date(cursor: &mut IsoCursor) -> ParseResult { + let year = parse_date_year(cursor)?; + let divided = cursor + .peek() + .map(|ch| *ch == '-') + .ok_or_else(|| Error::AbruptEnd)?; + + if divided { + cursor.advance(); + } + + let month = parse_date_month(cursor)?; + + if cursor.peek().map(|ch| *ch == '-').unwrap_or(false) { + if !divided { + return Err(LexError::syntax( + "Invalid date separator", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance(); + } + + let day = parse_date_day(cursor)?; + + Ok(DateRecord { year, month, day }) +} + +// ==== Unit Parsers ==== +// (referring to Year, month, day, hour, sec, etc...) + +fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult { + if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { + let year_start = cursor.pos(); + let sign = if *cursor.peek().unwrap() == '+' { 1 } else { -1 }; + + cursor.advance(); + + for _ in 0..6 { + let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; + if !year_digit.is_ascii_digit() { + return Err(Error::lex(LexError::syntax( + "DateYear must contain digit", + Position::new(1, (cursor.pos() + 1) as u32), + ))); + } + cursor.advance(); + } + + let year_string = cursor.slice(year_start + 1, cursor.pos()); + let year_value = year_string.parse::().map_err(|e| Error::general(e.to_string(), Position::new(1, (year_start + 1) as u32)))?; + + // 13.30.1 Static Semantics: Early Errors + // + // It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000). + if sign == -1 && year_value == 0 { + return Err(Error::lex(LexError::syntax( + "Cannot have negative 0 years.", + Position::new(1, (year_start + 1) as u32), + ))); + } + + return Ok(sign * year_value); + } + + let year_start = cursor.pos(); + + for _ in 0..4 { + let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; + if !year_digit.is_ascii_digit() { + return Err(LexError::syntax( + "DateYear must contain digit", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance(); + } + + let year_string = cursor.slice(year_start, cursor.pos()); + let year_value = year_string.parse::().map_err(|e| Error::general(e.to_string(), Position::new(1, (cursor.pos() + 1) as u32)))?; + + return Ok(year_value); +} + +fn parse_date_month(cursor: &mut IsoCursor) -> ParseResult { + let month_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, (cursor.pos() + 1) as u32)))?; + if !(1..=12).contains(&month_value) { + return Err(LexError::syntax( + "DateMonth must be in a range of 1-12", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance_n(2); + Ok(month_value) +} + +fn parse_date_day(cursor: &mut IsoCursor) -> ParseResult { + let day_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() as u32)))?; + if !(1..=31).contains(&day_value) { + return Err(LexError::syntax( + "DateDay must be in a range of 1-31", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance_n(2); + Ok(day_value) +} diff --git a/boa_parser/src/temporal/grammar.rs b/boa_parser/src/temporal/grammar.rs new file mode 100644 index 00000000000..68413e0bc76 --- /dev/null +++ b/boa_parser/src/temporal/grammar.rs @@ -0,0 +1,55 @@ +//! ISO8601 specific grammar checks. + +/// Checks if char is a `AKeyLeadingChar`. +#[inline] +pub(crate) fn is_a_key_leading_char(ch: &char) -> bool { + ch.is_ascii_lowercase() || *ch == '_' +} + +/// Checks if char is an `AKeyChar`. +#[inline] +pub(crate) fn is_a_key_char(ch: &char) -> bool { + is_a_key_leading_char(ch) || ch.is_ascii_digit() || *ch == '-' +} + +/// Checks if char is an `AnnotationValueComponent`. +pub(crate) fn is_annotation_value_component(ch: &char) -> bool { + ch.is_ascii_digit() || ch.is_ascii_alphabetic() +} + +/// Checks if char is a `TZLeadingChar`. +#[inline] +pub(crate) fn is_tz_leading_char(ch: &char) -> bool { + ch.is_ascii_alphabetic() || *ch == '_' || *ch == '.' +} + +/// Checks if char is a `TZChar`. +#[inline] +pub(crate) fn is_tz_char(ch: &char) -> bool { + is_tz_leading_char(ch) || ch.is_ascii_digit() || *ch == '-' || *ch == '+' +} + +/// Checks if char is an ascii sign. +pub(crate) fn is_ascii_sign(ch: &char) -> bool { + *ch == '+' || *ch == '-' +} + +/// Checks if char is an ascii sign or U+2212 +pub(crate) fn is_sign(ch: &char) -> bool { + is_ascii_sign(ch) || *ch == '\u{2212}' +} + +/// Checks if char is a `DateTimeSeparator`. +pub(crate) fn is_date_time_separator(ch: &char) -> bool { + *ch == 'T' || *ch == 't' || *ch == '\u{0020}' +} + +/// Checks if char is a `UtcDesignator`. +pub(crate) fn is_utc_designator(ch: &char) -> bool { + *ch == 'Z' || *ch == 'z' +} + +/// Checks if char is a `DecimalSeparator`. +pub(crate) fn is_decimal_separator(ch: &char) -> bool { + *ch == '.' || *ch == ',' +} \ No newline at end of file diff --git a/boa_parser/src/temporal/mod.rs b/boa_parser/src/temporal/mod.rs new file mode 100644 index 00000000000..4ef6bfa3217 --- /dev/null +++ b/boa_parser/src/temporal/mod.rs @@ -0,0 +1,97 @@ +//! Implementation of ISO8601 grammar lexing/parsing +#[allow(unused_variables)] + +use crate::error::ParseResult; + +mod tests; +mod time; +mod time_zone; +mod grammar; +mod date_time; +mod annotations; + +use boa_ast::temporal::{AnnotatedDateTime, TzIdentifier}; + +// TODO: optimize where possible. +// +// NOTE: +// Rough max length source given iso calendar and no extraneous annotations +// is ~100 characters (+10-20 for some calendars): +// +001970-01-01T00:00:00.000000000+00:00:00.000000000[!America/Argentina/ComodRivadavia][!u-ca=iso8601] + +/// Parse a `TemporalDateTimeString`. +#[derive(Debug, Clone, Copy)] +pub struct TemporalDateTimeString; + +impl TemporalDateTimeString { + /// Parses a targeted `DateTimeString`. + pub fn parse(zoned: bool, cursor: &mut IsoCursor) -> ParseResult { + date_time::parse_annotated_date_time(zoned, cursor) + } +} + +/// Parse a `TemporalTimeZoneString` +#[derive(Debug, Clone, Copy)] +pub struct TemporalTimeZoneString; + +impl TemporalTimeZoneString { + /// Parses a targeted `TimeZoneString`. + pub fn parse(cursor: &mut IsoCursor) -> ParseResult { + time_zone::parse_tz_identifier(cursor) + } +} + +// ==== Mini cursor implementation for ISO8601 targets ==== + +/// `IsoCursor` is a small cursor implementation for parsing ISO8601 grammar. +#[derive(Debug)] +pub struct IsoCursor { + pos: usize, + source: Vec, +} + +impl IsoCursor { + /// Create a new cursor from a source `String` value. + pub fn new(source: String) -> Self { + Self { + pos: 0, + source: source.chars().collect(), + } + } + + /// Returns a string value from a slice of the cursor. + fn slice(&self, start: usize, end: usize) -> String { + self.source[start..end].into_iter().collect() + } + + /// Get current position + const fn pos(&self) -> usize { + self.pos + } + + /// Peek the value at the current position. + fn peek(&self) -> Option<&char> { + self.source.get(self.pos) + } + + /// Peek the value at n len from current. + fn peek_n(&self, n: usize) -> Option<&char> { + self.source.get(self.pos + n) + } + + /// Advances the cursor's position and returns the new character. + fn next(&mut self) -> Option<&char> { + self.advance(); + self.source.get(self.pos) + } + + /// Advances the cursor's position by 1. + fn advance(&mut self) { + self.pos += 1; + } + + /// Advances the cursor's position by `n`. + fn advance_n(&mut self, n: usize) { + self.pos += n; + } +} diff --git a/boa_parser/src/temporal/tests.rs b/boa_parser/src/temporal/tests.rs new file mode 100644 index 00000000000..83cea1c6c12 --- /dev/null +++ b/boa_parser/src/temporal/tests.rs @@ -0,0 +1,98 @@ + +#[test] +fn temporal_parser_basic() { + use super::{IsoCursor, TemporalDateTimeString}; + let basic = "20201108"; + let basic_separated = "2020-11-08"; + + let basic_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic.to_string())).unwrap(); + + let sep_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic_separated.to_string())).unwrap(); + + assert_eq!(basic_result.date_time.date.year, 2020); + assert_eq!(basic_result.date_time.date.month, 11); + assert_eq!(basic_result.date_time.date.day, 8); + assert_eq!(basic_result.date_time.date.year, sep_result.date_time.date.year); + assert_eq!(basic_result.date_time.date.month, sep_result.date_time.date.month); + assert_eq!(basic_result.date_time.date.day, sep_result.date_time.date.day); +} + +#[test] +fn temporal_date_time_max() { + use super::{IsoCursor, TemporalDateTimeString}; + // Fractions not accurate, but for testing purposes. + let date_time = "+002020-11-08T12:28:32.329402834-03:00:00.123456789[!America/Argentina/ComodRivadavia][!u-ca=iso8601]"; + + let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(date_time.to_string())).unwrap(); + + let time_results = &result.date_time.time.unwrap(); + + assert_eq!(time_results.hour, 12); + assert_eq!(time_results.minute, 28); + assert_eq!(time_results.second, 32.329402834); + + let offset_results = &result.date_time.offset.unwrap(); + + assert_eq!(offset_results.sign, -1); + assert_eq!(offset_results.hour, 3); + assert_eq!(offset_results.minute, 0); + assert_eq!(offset_results.second, 0.123456789); + + let tz = &result.tz_annotation.unwrap(); + + assert!(tz.critical); + + match &tz.tz { + boa_ast::temporal::TzIdentifier::TzIANAName(id) => assert_eq!(id, "America/Argentina/ComodRivadavia"), + _=> unreachable!(), + } + + let annotations = &result.annotations.unwrap(); + + assert!(annotations.contains_key("u-ca")); + assert_eq!(annotations.get("u-ca"), Some(&(true, "iso8601".to_string()))); +} + +#[test] +fn temporal_year_parsing() { + use super::{IsoCursor, TemporalDateTimeString}; + let long = "+002020-11-08"; + let bad_year = "-000000-11-08"; + + let result_good = TemporalDateTimeString::parse(false, &mut IsoCursor::new(long.to_string())).unwrap(); + assert_eq!(result_good.date_time.date.year, 2020); + + let err_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(bad_year.to_string())); + assert!(err_result.is_err()); +} + +#[test] +fn temporal_annotated_date_time() { + use super::{IsoCursor, TemporalDateTimeString}; + let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]"; + let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]"; + + let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic.to_string())).unwrap(); + + if let Some(tz) = &result.tz_annotation { + match &tz.tz { + boa_ast::temporal::TzIdentifier::TzIANAName(id) => assert_eq!(id, "America/Argentina/ComodRivadavia"), + _=> unreachable!(), + } + } + + if let Some(annotations) = &result.annotations { + assert!(annotations.contains_key("u-ca")); + assert_eq!(annotations.get("u-ca"), Some(&(false, "iso8601".to_string()))) + } + + let omit_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(omitted.to_string())).unwrap(); + + assert!(&omit_result.tz_annotation.is_none()); + + if let Some(annotations) = &omit_result.annotations { + assert!(annotations.contains_key("u-ca")); + assert_eq!(annotations.get("u-ca"), Some(&(false, "iso8601".to_string()))) + } + +} diff --git a/boa_parser/src/temporal/time.rs b/boa_parser/src/temporal/time.rs new file mode 100644 index 00000000000..7bc7efcc5ef --- /dev/null +++ b/boa_parser/src/temporal/time.rs @@ -0,0 +1,105 @@ +//! Parsing of ISO8601 Time Values + +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, +}; +use super::{IsoCursor, grammar::*}; + +use boa_ast::{ + Position, + temporal::TimeSpec +}; + +/// Parse `TimeSpec` +pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult { + let hour = parse_hour(cursor)?; + let mut separator = false; + + if cursor.peek().map(|ch| *ch == ':' || ch.is_ascii_digit()).unwrap_or(false) { + if cursor.peek().map(|ch| *ch == ':').unwrap_or(false) { + separator = true; + cursor.advance(); + } + } else { + return Ok(TimeSpec{ hour, minute: 0, second: 0.0 }) + } + + let minute = parse_minute_second(cursor, false)?; + + if cursor.peek().map(|ch| *ch == ':' || ch.is_ascii_digit()).unwrap_or(false) { + let is_time_separator = cursor.peek().map(|ch| *ch == ':').unwrap_or(false); + if separator && is_time_separator { + cursor.advance(); + } else if is_time_separator { + return Err(LexError::syntax("Invalid TimeSeparator", Position::new(1, cursor.pos() as u32)).into()); + } + } else { + return Ok(TimeSpec{ hour, minute, second: 0.0 }) + } + + let second = parse_minute_second(cursor, true)?; + + let double = if cursor.peek().map(|ch| is_decimal_separator(ch)).unwrap_or(false) { + f64::from(second) + parse_fraction(cursor)? + } else { + f64::from(second) + }; + + Ok(TimeSpec { hour, minute, second: double }) +} + +pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult { + let hour_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() as u32)))?; + if !(0..=23).contains(&hour_value) { + return Err(LexError::syntax( + "Hour must be in a range of 0-23", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance_n(2); + Ok(hour_value) +} + +// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`. +/// Parse `MinuteSecond` +pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> ParseResult { + let min_sec_value = cursor + .slice(cursor.pos(), cursor.pos() + 2) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() as u32)))?; + let valid_range = if inclusive { 0..=60 } else { 0..=59 }; + if !valid_range.contains(&min_sec_value) { + return Err(LexError::syntax( + "MinuteSecond must be in a range of 0-59", + Position::new(1, (cursor.pos() + 1) as u32), + ).into()); + } + cursor.advance_n(2); + Ok(min_sec_value) +} + +/// Parse a `Fraction` value +/// +/// This is primarily used in ISO8601 to add percision past +/// a second. +pub(crate) fn parse_fraction(cursor: &mut IsoCursor) -> ParseResult { + let fraction_start = cursor.pos(); + cursor.advance(); + + // TODO: implement error for going past 9 Digit values. + while let Some(ch) = cursor.next() { + if !ch.is_ascii_digit() { + let frac = cursor + .slice(fraction_start, cursor.pos()) + .parse::() + .map_err(|e| Error::general(e.to_string(), Position::new(1, (cursor.pos() - 1) as u32)))?; + return Ok(frac) + } + } + + return Err(Error::AbruptEnd) +} diff --git a/boa_parser/src/temporal/time_zone.rs b/boa_parser/src/temporal/time_zone.rs new file mode 100644 index 00000000000..aac801eb6b6 --- /dev/null +++ b/boa_parser/src/temporal/time_zone.rs @@ -0,0 +1,183 @@ +//! ISO8601 parsing for Time Zone and Offset data. + +use crate::{ + error::{Error, ParseResult}, + lexer::Error as LexError, +}; +use super::{ + IsoCursor, + time::{parse_minute_second, parse_fraction, parse_hour}, + grammar::*, +}; + +use boa_ast::{Position, temporal::{TimeZoneAnnotation, UtcOffset, TzIdentifier}}; + +// ==== Time Zone Annotation Parsing ==== + +pub(crate) fn parse_ambiguous_tz_annotation(cursor: &mut IsoCursor) -> ParseResult> { + // Peek position + 1 to check for critical flag. + let mut current_peek = 1; + let critical = cursor + .peek_n(current_peek) + .map(|ch| *ch == '!') + .ok_or_else(|| Error::AbruptEnd)?; + + // Advance cursor if critical flag present. + if critical { + current_peek += 1; + } + + let leading_char = cursor + .peek_n(current_peek) + .ok_or_else(|| Error::AbruptEnd)?; + + match is_tz_leading_char(leading_char) || is_sign(leading_char) { + // Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`. + true if is_a_key_leading_char(leading_char) => { + let mut peek_pos = current_peek + 1; + while let Some(ch) = cursor.peek_n(peek_pos) { + if *ch == '/' || (is_tz_char(ch) && !is_a_key_char(ch)) { + let tz = parse_tz_annotation(cursor)?; + return Ok(Some(tz)); + } else if *ch == '=' || (is_a_key_char(ch) && !is_tz_char(ch)) { + return Ok(None); + } else if *ch == ']' { + return Err(LexError::syntax( + "Invalid Annotation", + Position::new(1, (peek_pos + 1) as u32), + ).into()); + } + + peek_pos += 1; + } + Err(Error::AbruptEnd) + } + true => { + let tz = parse_tz_annotation(cursor)?; + Ok(Some(tz)) + } + false if is_a_key_leading_char(leading_char) => { + Ok(None) + } + _ => Err(Error::lex(LexError::syntax( + "Unexpected character in ambiguous annotation.", + Position::new(1, (cursor.pos() + 1) as u32), + ))), + } +} + +fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult { + assert!(*cursor.peek().unwrap() == '['); + + let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; + let critical = *potential_critical == '!'; + + if critical { + cursor.advance(); + } + + let tz = parse_tz_identifier(cursor)?; + + if !cursor.peek().map(|ch| *ch == ']').unwrap_or(false) { + return Err(LexError::syntax("Invalid TimeZoneAnnotation.", Position::new(1, (cursor.pos() + 1) as u32)).into()) + } + + cursor.advance(); + + Ok(TimeZoneAnnotation { critical, tz }) +} + +pub(crate) fn parse_tz_identifier(cursor: &mut IsoCursor) -> ParseResult { + let is_iana = cursor.peek().map(|ch| is_tz_leading_char(ch)).ok_or_else(|| Error::AbruptEnd)?; + let is_offset = cursor.peek().map(|ch| is_sign(ch)).unwrap_or(false); + + if is_iana { + let iana_name = parse_tz_iana_name(cursor)?; + return Ok(TzIdentifier::TzIANAName(iana_name)); + } else if is_offset { + let offset = parse_utc_offset_minute_precision(cursor)?; + return Ok(TzIdentifier::UtcOffset(offset)) + } + + Err(LexError::syntax("Invalid leading character for a TimeZoneIdentifier", Position::new(1, (cursor.pos() + 1) as u32)).into()) +} + +/// Parse a `TimeZoneIANAName` Parse Node +fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult { + let tz_name_start = cursor.pos(); + while let Some(potential_value_char) = cursor.next() { + if *potential_value_char == '/' { + if !cursor + .peek_n(1) + .map(|ch| is_tz_char(ch)) + .unwrap_or(false) + { + return Err(LexError::syntax( + "Missing TimeZoneIANANameComponent after '/'", + Position::new(1, (cursor.pos() + 2) as u32), + ).into()); + } + continue; + } + + if !is_tz_char(potential_value_char) { + // Return the valid TimeZoneIANAName + return Ok(cursor.slice(tz_name_start, cursor.pos())); + } + + } + + return Err(Error::AbruptEnd); +} + +// ==== Utc Offset Parsing ==== + +/// Parse a full precision `UtcOffset` +pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult { + if cursor.peek().map(|ch| is_utc_designator(ch)).unwrap_or(false) { + cursor.advance(); + return Ok(UtcOffset { utc: true, sign: 1, hour: 0, minute: 0, second: 0.0 }) + } + + let separated = cursor.peek_n(3).map(|ch| *ch == ':').unwrap_or(false); + + let mut utc_to_minute = parse_utc_offset_minute_precision(cursor)?; + + if cursor.peek().map(|ch| *ch == ':').unwrap_or(false) { + if !separated { + return Err(LexError::syntax("Unexpected TimeSeparator", Position::new(1, cursor.pos() as u32)).into()) + } + cursor.advance(); + } + + let sec = parse_minute_second(cursor, true)?; + + let double = if cursor.peek().map(|ch| is_decimal_separator(ch)).unwrap_or(false) { + f64::from(sec) + parse_fraction(cursor)? + } else { + f64::from(sec) + }; + + utc_to_minute.second = double; + Ok(utc_to_minute) +} + +/// Parse an `UtcOffsetMinutePrecision` node +pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> ParseResult { + let sign = if let Some(ch) = cursor.next() { if *ch == '+' { 1_i8 } else { -1_i8 }} else {return Err(Error::AbruptEnd)}; + let hour = parse_hour(cursor)?; + + // If at the end of the utc, then return. + if cursor.peek().map(|ch| !(ch.is_ascii_digit() || *ch == ':')).ok_or_else(|| Error::AbruptEnd)? { + return Ok(UtcOffset { utc: false, sign, hour, minute: 0, second: 0.0 }) + } + + // Advance cursor beyond any TimeSeparator + if cursor.peek().map(|ch| *ch == ':').unwrap_or(false) { + cursor.advance(); + } + + let minute = parse_minute_second(cursor, false)?; + + return Ok(UtcOffset { utc: false, sign, hour, minute, second: 0.0 }) +} \ No newline at end of file