Browse Source

Migrate iso parsing to boa_temporal (#3500)

pull/3497/head
Kevin 1 year ago committed by GitHub
parent
commit
c113b74b09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      boa_ast/Cargo.toml
  2. 2
      boa_ast/src/lib.rs
  3. 2
      boa_engine/Cargo.toml
  4. 1
      boa_engine/src/builtins/temporal/error.rs
  5. 37
      boa_engine/src/builtins/temporal/plain_date/mod.rs
  6. 4
      boa_engine/src/builtins/temporal/time_zone/mod.rs
  7. 1
      boa_parser/Cargo.toml
  8. 2
      boa_parser/src/lib.rs
  9. 32
      boa_temporal/src/date.rs
  10. 41
      boa_temporal/src/datetime.rs
  11. 55
      boa_temporal/src/duration.rs
  12. 15
      boa_temporal/src/error.rs
  13. 34
      boa_temporal/src/iso.rs
  14. 1
      boa_temporal/src/lib.rs
  15. 72
      boa_temporal/src/parser/annotations.rs
  16. 153
      boa_temporal/src/parser/date_time.rs
  17. 107
      boa_temporal/src/parser/duration.rs
  18. 0
      boa_temporal/src/parser/grammar.rs
  19. 151
      boa_temporal/src/parser/mod.rs
  20. 33
      boa_temporal/src/parser/nodes.rs
  21. 92
      boa_temporal/src/parser/tests.rs
  22. 48
      boa_temporal/src/parser/time.rs
  23. 79
      boa_temporal/src/parser/time_zone.rs

1
boa_ast/Cargo.toml

@ -13,7 +13,6 @@ rust-version.workspace = true
[features]
serde = ["dep:serde", "boa_interner/serde", "bitflags/serde", "num-bigint/serde"]
arbitrary = ["dep:arbitrary", "boa_interner/arbitrary", "num-bigint/arbitrary"]
temporal = []
[dependencies]
boa_interner.workspace = true

2
boa_ast/src/lib.rs

@ -91,8 +91,6 @@ pub mod operations;
pub mod pattern;
pub mod property;
pub mod statement;
#[cfg(feature = "temporal")]
pub mod temporal;
pub mod visitor;
use boa_interner::{Interner, ToIndentedString, ToInternedString};

2
boa_engine/Cargo.toml

@ -47,7 +47,7 @@ trace = ["js"]
annex-b = ["boa_parser/annex-b"]
# Stage 3 proposals
temporal = ["boa_parser/temporal", "dep:icu_calendar"]
temporal = ["dep:icu_calendar"]
# Enable experimental features, like Stage 3 proposals.
experimental = ["temporal"]

1
boa_engine/src/builtins/temporal/error.rs

@ -8,6 +8,7 @@ impl From<TemporalError> for JsNativeError {
ErrorKind::Range => JsNativeError::range().with_message(value.message()),
ErrorKind::Type => JsNativeError::typ().with_message(value.message()),
ErrorKind::Generic => JsNativeError::error().with_message(value.message()),
ErrorKind::Syntax => JsNativeError::syntax().with_message(value.message()),
}
}
}

37
boa_engine/src/builtins/temporal/plain_date/mod.rs

@ -1,8 +1,6 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object.
#![allow(dead_code, unused_variables)]
use std::str::FromStr;
use crate::{
builtins::{
options::{get_option, get_options_object},
@ -16,14 +14,8 @@ use crate::{
string::{common::StaticJsStrings, utf16},
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_parser::temporal::{IsoCursor, TemporalDateTimeString};
use boa_profiler::Profiler;
use boa_temporal::{
calendar::{AvailableCalendars, CalendarSlot},
date::Date as InnerDate,
datetime::DateTime,
options::ArithmeticOverflow,
};
use boa_temporal::{date::Date as InnerDate, datetime::DateTime, options::ArithmeticOverflow};
use super::calendar;
@ -517,32 +509,17 @@ pub(crate) fn to_temporal_date(
};
// 6. Let result be ? ParseTemporalDateString(item).
let result = TemporalDateTimeString::parse(
false,
&mut IsoCursor::new(&date_like_string.to_std_string_escaped()),
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
// 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true.
// 8. Let calendar be result.[[Calendar]].
// 9. If calendar is undefined, set calendar to "iso8601".
let identifier = result.date.calendar.unwrap_or("iso8601".to_string());
// 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception.
let _ = AvailableCalendars::from_str(identifier.to_ascii_lowercase().as_str())?;
// 11. Set calendar to the ASCII-lowercase of calendar.
let calendar = CalendarSlot::Identifier(identifier.to_ascii_lowercase());
// 12. Perform ? ToTemporalOverflow(options).
let _ = get_option::<ArithmeticOverflow>(&options_obj, utf16!("overflow"), context)?;
// 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar).
Ok(PlainDate::new(InnerDate::new(
result.date.year,
result.date.month,
result.date.day,
calendar,
ArithmeticOverflow::Reject,
)?))
let result = date_like_string
.to_std_string_escaped()
.parse::<InnerDate>()
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(PlainDate::new(result))
}

4
boa_engine/src/builtins/temporal/time_zone/mod.rs

@ -364,10 +364,10 @@ 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<i64> {
use boa_parser::temporal::{IsoCursor, TemporalTimeZoneString};
use boa_temporal::parser::{Cursor, TemporalTimeZoneString};
// 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset).
let parse_result = TemporalTimeZoneString::parse(&mut IsoCursor::new(offset_string))?;
let parse_result = TemporalTimeZoneString::parse(&mut Cursor::new(offset_string))?;
// 2. Assert: parseResult is not a List of errors.
// 3. Assert: parseResult contains a TemporalSign Parse Node.

1
boa_parser/Cargo.toml

@ -25,7 +25,6 @@ icu_properties.workspace = true
[features]
annex-b = []
temporal = ["boa_ast/temporal"]
[lints]
workspace = true

2
boa_parser/src/lib.rs

@ -28,8 +28,6 @@ pub mod error;
pub mod lexer;
pub mod parser;
mod source;
#[cfg(feature = "temporal")]
pub mod temporal;
pub use error::Error;
pub use lexer::Lexer;

32
boa_temporal/src/date.rs

@ -1,14 +1,15 @@
//! The `PlainDate` representation.
use crate::{
calendar::CalendarSlot,
calendar::{AvailableCalendars, CalendarSlot},
datetime::DateTime,
duration::{DateDuration, Duration},
iso::{IsoDate, IsoDateSlots},
options::{ArithmeticOverflow, TemporalUnit},
TemporalResult,
parser::parse_date_time,
TemporalError, TemporalResult,
};
use std::any::Any;
use std::{any::Any, str::FromStr};
/// The `Temporal.PlainDate` equivalent
#[derive(Debug, Default, Clone)]
@ -218,3 +219,28 @@ impl Date {
self.contextual_difference_date(other, largest_unit, &mut ())
}
}
// ==== Trait impls ====
impl FromStr for Date {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_date_time(s)?;
let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned());
let _ = AvailableCalendars::from_str(calendar.to_ascii_lowercase().as_str())?;
let date = IsoDate::new(
parse_record.date.year,
parse_record.date.month,
parse_record.date.day,
ArithmeticOverflow::Reject,
)?;
Ok(Self::new_unchecked(
date,
CalendarSlot::Identifier(calendar),
))
}
}

41
boa_temporal/src/datetime.rs

@ -1,10 +1,13 @@
//! Temporal implementation of `DateTime`
use std::str::FromStr;
use crate::{
calendar::CalendarSlot,
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime},
options::ArithmeticOverflow,
TemporalResult,
parser::parse_date_time,
TemporalError, TemporalResult,
};
/// The `DateTime` struct.
@ -93,3 +96,39 @@ impl DateTime {
&self.calendar
}
}
// ==== Trait impls ====
impl FromStr for DateTime {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_date_time(s)?;
let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned());
let time = if let Some(time) = parse_record.time {
IsoTime::from_components(
i32::from(time.hour),
i32::from(time.minute),
i32::from(time.second),
time.fraction,
)?
} else {
IsoTime::default()
};
let date = IsoDate::new(
parse_record.date.year,
parse_record.date.month,
parse_record.date.day,
ArithmeticOverflow::Reject,
)?;
Ok(Self::new_unchecked(
date,
time,
CalendarSlot::Identifier(calendar),
))
}
}

55
boa_temporal/src/duration.rs

@ -6,11 +6,12 @@ use crate::{
date::Date,
datetime::DateTime,
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit},
parser::{duration::parse_duration, Cursor},
utils,
zoneddatetime::ZonedDateTime,
TemporalError, TemporalResult, NS_PER_DAY,
};
use std::any::Any;
use std::{any::Any, str::FromStr};
// ==== `DateDuration` ====
@ -1688,3 +1689,55 @@ impl Duration {
Ok(result)
}
}
// ==== FromStr trait impl ====
impl FromStr for Duration {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_duration(&mut Cursor::new(s))?;
let minutes = if parse_record.time.fhours > 0.0 {
parse_record.time.fhours * 60.0
} else {
f64::from(parse_record.time.minutes)
};
let seconds = if parse_record.time.fminutes > 0.0 {
parse_record.time.fminutes * 60.0
} else if parse_record.time.seconds > 0 {
f64::from(parse_record.time.seconds)
} else {
minutes.rem_euclid(1.0) * 60.0
};
let milliseconds = if parse_record.time.fseconds > 0.0 {
parse_record.time.fseconds * 1000.0
} else {
seconds.rem_euclid(1.0) * 1000.0
};
let micro = milliseconds.rem_euclid(1.0) * 1000.0;
let nano = micro.rem_euclid(1.0) * 1000.0;
let sign = if parse_record.sign { 1f64 } else { -1f64 };
Ok(Self {
date: DateDuration::new(
f64::from(parse_record.date.years) * sign,
f64::from(parse_record.date.months) * sign,
f64::from(parse_record.date.weeks) * sign,
f64::from(parse_record.date.days) * sign,
),
time: TimeDuration::new(
f64::from(parse_record.time.hours) * sign,
minutes.floor() * sign,
seconds.floor() * sign,
milliseconds.floor() * sign,
micro.floor() * sign,
nano.floor() * sign,
),
})
}
}

15
boa_temporal/src/error.rs

@ -12,6 +12,8 @@ pub enum ErrorKind {
Type,
/// RangeError
Range,
/// SyntaxError
Syntax,
}
impl fmt::Display for ErrorKind {
@ -20,6 +22,7 @@ impl fmt::Display for ErrorKind {
Self::Generic => "Error",
Self::Type => "TypeError",
Self::Range => "RangeError",
Self::Syntax => "SyntaxError",
}
.fmt(f)
}
@ -61,6 +64,18 @@ impl TemporalError {
Self::new(ErrorKind::Type)
}
/// Create a syntax error.
#[must_use]
pub fn syntax() -> Self {
Self::new(ErrorKind::Syntax)
}
/// Create an abrupt end error.
#[must_use]
pub fn abrupt_end() -> Self {
Self::syntax().with_message("Abrupt end to parsing target.")
}
/// Add a message to the error.
#[must_use]
pub fn with_message<S>(mut self, msg: S) -> Self

34
boa_temporal/src/iso.rs

@ -209,12 +209,12 @@ impl IsoDate {
/// time slots.
#[derive(Debug, Default, Clone, Copy)]
pub struct IsoTime {
hour: i32, // 0..=23
minute: i32, // 0..=59
second: i32, // 0..=59
millisecond: i32, // 0..=999
microsecond: i32, // 0..=999
nanosecond: i32, // 0..=999
pub(crate) hour: i32, // 0..=23
pub(crate) minute: i32, // 0..=59
pub(crate) second: i32, // 0..=59
pub(crate) millisecond: i32, // 0..=999
pub(crate) microsecond: i32, // 0..=999
pub(crate) nanosecond: i32, // 0..=999
}
impl IsoTime {
@ -281,6 +281,28 @@ impl IsoTime {
}
}
/// Returns an `IsoTime` based off parse components.
pub(crate) fn from_components(
hour: i32,
minute: i32,
second: i32,
fraction: f64,
) -> TemporalResult<Self> {
let millisecond = fraction * 1000f64;
let micros = millisecond.rem_euclid(1f64) * 1000f64;
let nanos = micros.rem_euclid(1f64).mul_add(1000f64, 0.5).floor();
Self::new(
hour,
minute,
second,
millisecond as i32,
micros as i32,
nanos as i32,
ArithmeticOverflow::Reject,
)
}
/// Checks if the time is a valid `IsoTime`
pub(crate) fn is_valid(&self) -> bool {
if !(0..=23).contains(&self.hour) {

1
boa_temporal/src/lib.rs

@ -36,6 +36,7 @@ pub mod fields;
pub mod iso;
pub mod month_day;
pub mod options;
pub mod parser;
pub mod time;
pub(crate) mod utils;
pub mod year_month;

72
boa_parser/src/temporal/annotations.rs → boa_temporal/src/parser/annotations.rs

@ -1,20 +1,17 @@
/// Parsing for Temporal's `Annotations`.
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
temporal::{
parser::{
grammar::{
is_a_key_char, is_a_key_leading_char, is_annotation_close,
is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag,
},
time_zone,
time_zone::TimeZoneAnnotation,
IsoCursor,
Cursor,
},
TemporalError, TemporalResult,
};
use boa_ast::{Position, Span};
use super::grammar::{is_annotation_open, is_hyphen};
/// A `KeyValueAnnotation` Parse Node.
@ -37,20 +34,14 @@ pub(crate) struct AnnotationSet {
/// Parse a `TimeZoneAnnotation` `Annotations` set
pub(crate) fn parse_annotation_set(
zoned: bool,
cursor: &mut IsoCursor,
) -> ParseResult<AnnotationSet> {
cursor: &mut Cursor,
) -> TemporalResult<AnnotationSet> {
// 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),
Position::new(1, cursor.pos() + 2),
),
"iso8601 ZonedDateTime requires a TimeZoneAnnotation.",
));
return Err(TemporalError::syntax()
.with_message("iso8601 ZonedDateTime requires a TimeZoneAnnotation."));
}
// Parse any `Annotations`
@ -77,12 +68,11 @@ pub(crate) struct RecognizedAnnotations {
}
/// Parse any number of `KeyValueAnnotation`s
pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult<RecognizedAnnotations> {
pub(crate) fn parse_annotations(cursor: &mut Cursor) -> TemporalResult<RecognizedAnnotations> {
let mut annotations = RecognizedAnnotations::default();
let mut calendar_crit = false;
while cursor.check_or(false, is_annotation_open) {
let start = Position::new(1, cursor.pos() + 1);
let kv = parse_kv_annotation(cursor)?;
if &kv.key == "u-ca" {
@ -93,13 +83,12 @@ pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult<Recognize
}
if calendar_crit || kv.critical {
return Err(Error::general(
return Err(TemporalError::syntax().with_message(
"Cannot have critical flag with duplicate calendar annotations",
start,
));
}
} else if kv.critical {
return Err(Error::general("Unrecognized critical annotation.", start));
return Err(TemporalError::syntax().with_message("Unrecognized critical annotation."));
}
}
@ -107,22 +96,18 @@ pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult<Recognize
}
/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair.
fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult<KeyValueAnnotation> {
fn parse_kv_annotation(cursor: &mut Cursor) -> TemporalResult<KeyValueAnnotation> {
debug_assert!(cursor.check_or(false, is_annotation_open));
let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?;
let potential_critical = cursor.next().ok_or_else(TemporalError::abrupt_end)?;
let (leading_char, critical) = if is_critical_flag(potential_critical) {
(cursor.next().ok_or_else(|| Error::AbruptEnd)?, true)
(cursor.next().ok_or_else(TemporalError::abrupt_end)?, 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),
)
.into());
return Err(TemporalError::syntax().with_message("Invalid AnnotationKey leading character"));
}
// Parse AnnotationKey.
@ -147,7 +132,7 @@ fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult<KeyValueAnnotation
}
/// Parse an `AnnotationKey`.
fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult<String> {
fn parse_annotation_key(cursor: &mut Cursor) -> TemporalResult<String> {
let key_start = cursor.pos();
while let Some(potential_key_char) = cursor.next() {
// End of key.
@ -157,19 +142,15 @@ fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult<String> {
}
if !is_a_key_char(potential_key_char) {
return Err(LexError::syntax(
"Invalid AnnotationKey Character",
Position::new(1, cursor.pos() + 1),
)
.into());
return Err(TemporalError::syntax().with_message("Invalid AnnotationKey Character"));
}
}
Err(Error::AbruptEnd)
Err(TemporalError::abrupt_end())
}
/// Parse an `AnnotationValue`.
fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult<String> {
fn parse_annotation_value(cursor: &mut Cursor) -> TemporalResult<String> {
let value_start = cursor.pos();
while let Some(potential_value_char) = cursor.next() {
if is_annotation_close(potential_value_char) {
@ -182,24 +163,19 @@ fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult<String> {
.peek_n(1)
.map_or(false, is_annotation_value_component)
{
return Err(LexError::syntax(
"Missing AttributeValueComponent after '-'",
Position::new(1, cursor.pos() + 1),
)
.into());
return Err(TemporalError::syntax()
.with_message("Missing AttributeValueComponent after '-'"));
}
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),
)
.into());
return Err(
TemporalError::syntax().with_message("Invalid character in AnnotationValue")
);
}
}
Err(Error::AbruptEnd)
Err(TemporalError::abrupt_end())
}

153
boa_parser/src/temporal/date_time.rs → boa_temporal/src/parser/date_time.rs

@ -1,20 +1,19 @@
//! Parsing for Temporal's ISO8601 `Date` and `DateTime`.
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
temporal::{
parser::{
annotations,
grammar::{is_date_time_separator, is_sign, is_utc_designator},
nodes::TimeZone,
time,
time::TimeSpec,
time_zone, IsoCursor, IsoParseRecord,
time_zone, Cursor, IsoParseRecord,
},
TemporalError, TemporalResult,
};
use boa_ast::{temporal::TimeZone, Position, Span};
use super::grammar::{is_annotation_open, is_hyphen};
use bitflags::bitflags;
#[derive(Debug, Default, Clone)]
/// A `DateTime` Parse Node that contains the date, time, and offset info.
@ -38,6 +37,16 @@ pub(crate) struct DateRecord {
pub(crate) day: i32,
}
bitflags! {
/// Parsing flags for `AnnotatedDateTime` parsing.
#[derive(Debug, Clone, Copy)]
pub struct DateTimeFlags: u8 {
const ZONED = 0b0000_0001;
const TIME_REQ = 0b0000_0010;
const UTC_REQ = 0b0000_0100;
}
}
/// This function handles parsing for [`AnnotatedDateTime`][datetime],
/// [`AnnotatedDateTimeTimeRequred`][time], and
/// [`TemporalInstantString.`][instant] according to the requirements
@ -47,27 +56,22 @@ pub(crate) struct DateRecord {
/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
pub(crate) fn parse_annotated_date_time(
zoned: bool,
time_required: bool,
utc_required: bool,
cursor: &mut IsoCursor,
) -> ParseResult<IsoParseRecord> {
let date_time = parse_date_time(time_required, utc_required, cursor)?;
flags: DateTimeFlags,
cursor: &mut Cursor,
) -> TemporalResult<IsoParseRecord> {
let date_time = parse_date_time(
flags.contains(DateTimeFlags::TIME_REQ),
flags.contains(DateTimeFlags::UTC_REQ),
cursor,
)?;
// Peek Annotation presence
// Throw error if annotation does not exist and zoned is true, else return.
let annotation_check = cursor.check_or(false, is_annotation_open);
if !annotation_check {
if zoned {
return Err(Error::expected(
["TimeZoneAnnotation".into()],
"No Annotation",
Span::new(
Position::new(1, cursor.pos() + 1),
Position::new(1, cursor.pos() + 1),
),
"iso8601 grammar",
));
if flags.contains(DateTimeFlags::ZONED) {
return Err(TemporalError::syntax()
.with_message("ZonedDateTime must have a TimeZoneAnnotation."));
}
return Ok(IsoParseRecord {
@ -84,7 +88,8 @@ pub(crate) fn parse_annotated_date_time(
tz = tz_info;
}
let annotation_set = annotations::parse_annotation_set(zoned, cursor)?;
let annotation_set =
annotations::parse_annotation_set(flags.contains(DateTimeFlags::ZONED), cursor)?;
if let Some(annotated_tz) = annotation_set.tz {
tz = annotated_tz.tz;
@ -108,17 +113,14 @@ pub(crate) fn parse_annotated_date_time(
fn parse_date_time(
time_required: bool,
utc_required: bool,
cursor: &mut IsoCursor,
) -> ParseResult<DateTimeRecord> {
cursor: &mut Cursor,
) -> TemporalResult<DateTimeRecord> {
let date = parse_date(cursor)?;
// If there is no `DateTimeSeparator`, return date early.
if !cursor.check_or(false, is_date_time_separator) {
if time_required {
return Err(Error::general(
"Missing a required TimeSpec.",
Position::new(1, cursor.pos() + 1),
));
return Err(TemporalError::syntax().with_message("Missing a required Time target."));
}
return Ok(DateTimeRecord {
@ -139,10 +141,7 @@ fn parse_date_time(
Some(time_zone::parse_date_time_utc(cursor)?)
} else {
if utc_required {
return Err(Error::general(
"DateTimeUTCOffset is required.",
Position::new(1, cursor.pos() + 1),
));
return Err(TemporalError::syntax().with_message("DateTimeUTCOffset is required."));
}
None
};
@ -155,9 +154,11 @@ fn parse_date_time(
}
/// Parses `Date` record.
fn parse_date(cursor: &mut IsoCursor) -> ParseResult<DateRecord> {
fn parse_date(cursor: &mut Cursor) -> TemporalResult<DateRecord> {
let year = parse_date_year(cursor)?;
let divided = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?;
let divided = cursor
.check(is_hyphen)
.ok_or_else(TemporalError::abrupt_end)?;
if divided {
cursor.advance();
@ -167,11 +168,7 @@ fn parse_date(cursor: &mut IsoCursor) -> ParseResult<DateRecord> {
if cursor.check_or(false, is_hyphen) {
if !divided {
return Err(LexError::syntax(
"Invalid date separator",
Position::new(1, cursor.pos() + 1),
)
.into());
return Err(TemporalError::syntax().with_message("Invalid date separator"));
}
cursor.advance();
}
@ -182,8 +179,8 @@ fn parse_date(cursor: &mut IsoCursor) -> ParseResult<DateRecord> {
}
/// Determines if the string can be parsed as a `DateSpecYearMonth`.
pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult<bool> {
let mut ym_peek = if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) {
pub(crate) fn peek_year_month(cursor: &Cursor) -> TemporalResult<bool> {
let mut ym_peek = if is_sign(cursor.peek().ok_or_else(TemporalError::abrupt_end)?) {
7
} else {
4
@ -192,7 +189,7 @@ pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult<bool> {
if cursor
.peek_n(ym_peek)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?
.ok_or_else(TemporalError::abrupt_end)?
{
ym_peek += 1;
}
@ -207,7 +204,7 @@ pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult<bool> {
}
/// Parses a `DateSpecYearMonth`
pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> {
pub(crate) fn parse_year_month(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> {
let year = parse_date_year(cursor)?;
if cursor.check_or(false, is_hyphen) {
@ -220,11 +217,11 @@ pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)
}
/// Determines if the string can be parsed as a `DateSpecYearMonth`.
pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult<bool> {
pub(crate) fn peek_month_day(cursor: &Cursor) -> TemporalResult<bool> {
let mut md_peek = if cursor
.peek_n(1)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?
.ok_or_else(TemporalError::abrupt_end)?
{
4
} else {
@ -234,7 +231,7 @@ pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult<bool> {
if cursor
.peek_n(md_peek)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?
.ok_or_else(TemporalError::abrupt_end)?
{
md_peek += 1;
}
@ -249,21 +246,19 @@ pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult<bool> {
}
/// Parses a `DateSpecMonthDay`
pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> {
let dash_one = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?;
pub(crate) fn parse_month_day(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> {
let dash_one = cursor
.check(is_hyphen)
.ok_or_else(TemporalError::abrupt_end)?;
let dash_two = cursor
.peek_n(1)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?;
.ok_or_else(TemporalError::abrupt_end)?;
if dash_two && dash_one {
cursor.advance_n(2);
} else if dash_two && !dash_one {
return Err(LexError::syntax(
"MonthDay requires two dashes",
Position::new(1, cursor.pos()),
)
.into());
return Err(TemporalError::syntax().with_message("MonthDay requires two dashes"));
}
let month = parse_date_month(cursor)?;
@ -278,8 +273,8 @@ pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)>
// ==== Unit Parsers ====
fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> {
if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) {
fn parse_date_year(cursor: &mut Cursor) -> TemporalResult<i32> {
if is_sign(cursor.peek().ok_or_else(TemporalError::abrupt_end)?) {
let year_start = cursor.pos();
let sign = if cursor.check_or(false, |ch| ch == '+') {
1
@ -290,12 +285,9 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> {
cursor.advance();
for _ in 0..6 {
let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?;
let year_digit = cursor.peek().ok_or_else(TemporalError::abrupt_end)?;
if !year_digit.is_ascii_digit() {
return Err(Error::lex(LexError::syntax(
"DateYear must contain digit",
Position::new(1, cursor.pos() + 1),
)));
return Err(TemporalError::syntax().with_message("DateYear must contain digit"));
}
cursor.advance();
}
@ -303,16 +295,13 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> {
let year_string = cursor.slice(year_start + 1, cursor.pos());
let year_value = year_string
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, year_start + 1)))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
// 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),
)));
return Err(TemporalError::syntax().with_message("Cannot have negative 0 years."));
}
return Ok(sign * year_value);
@ -321,13 +310,9 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> {
let year_start = cursor.pos();
for _ in 0..4 {
let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?;
let year_digit = cursor.peek().ok_or_else(TemporalError::abrupt_end)?;
if !year_digit.is_ascii_digit() {
return Err(LexError::syntax(
"DateYear must contain digit",
Position::new(1, cursor.pos() + 1),
)
.into());
return Err(TemporalError::syntax().with_message("DateYear must contain digit"));
}
cursor.advance();
}
@ -335,38 +320,30 @@ fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> {
let year_string = cursor.slice(year_start, cursor.pos());
let year_value = year_string
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
Ok(year_value)
}
fn parse_date_month(cursor: &mut IsoCursor) -> ParseResult<i32> {
fn parse_date_month(cursor: &mut Cursor) -> TemporalResult<i32> {
let month_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
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),
)
.into());
return Err(TemporalError::syntax().with_message("DateMonth must be in a range of 1-12"));
}
cursor.advance_n(2);
Ok(month_value)
}
fn parse_date_day(cursor: &mut IsoCursor) -> ParseResult<i32> {
fn parse_date_day(cursor: &mut Cursor) -> TemporalResult<i32> {
let day_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
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),
)
.into());
return Err(TemporalError::syntax().with_message("DateDay must be in a range of 1-31"));
}
cursor.advance_n(2);
Ok(day_value)

107
boa_parser/src/temporal/duration.rs → boa_temporal/src/parser/duration.rs

@ -1,16 +1,14 @@
use boa_ast::Position;
use crate::{
error::{Error, ParseResult},
temporal::{
parser::{
grammar::{
is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator,
is_minute_designator, is_month_designator, is_second_designator, is_sign,
is_time_designator, is_week_designator, is_year_designator,
},
time::parse_fraction,
IsoCursor,
Cursor,
},
TemporalError, TemporalResult,
};
/// A ISO8601 `DurationRecord` Parse Node.
@ -54,8 +52,11 @@ pub(crate) struct TimeDuration {
pub(crate) fseconds: f64,
}
pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult<DurationParseRecord> {
let sign = if cursor.check(is_sign).ok_or_else(|| Error::AbruptEnd)? {
pub(crate) fn parse_duration(cursor: &mut Cursor) -> TemporalResult<DurationParseRecord> {
let sign = if cursor
.check(is_sign)
.ok_or_else(TemporalError::abrupt_end)?
{
let sign = cursor.check_or(false, |ch| ch == '+');
cursor.advance();
sign
@ -65,12 +66,11 @@ pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult<DurationPars
if !cursor
.check(is_duration_designator)
.ok_or_else(|| Error::AbruptEnd)?
.ok_or_else(TemporalError::abrupt_end)?
{
return Err(Error::general(
"DurationString missing DurationDesignator.",
Position::new(1, cursor.pos() + 1),
));
return Err(
TemporalError::syntax().with_message("DurationString missing DurationDesignator.")
);
}
cursor.advance();
@ -89,10 +89,7 @@ pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult<DurationPars
};
if cursor.peek().is_some() {
return Err(Error::general(
"Unrecognized value in DurationString.",
Position::new(1, cursor.pos()),
));
return Err(TemporalError::syntax().with_message("Unrecognized value in DurationString."));
}
Ok(DurationParseRecord {
@ -111,7 +108,7 @@ enum DateUnit {
Day,
}
pub(crate) fn parse_date_duration(cursor: &mut IsoCursor) -> ParseResult<DateDuration> {
pub(crate) fn parse_date_duration(cursor: &mut Cursor) -> TemporalResult<DateDuration> {
let mut date = DateDuration::default();
let mut previous_unit = DateUnit::None;
@ -125,52 +122,46 @@ pub(crate) fn parse_date_duration(cursor: &mut IsoCursor) -> ParseResult<DateDur
let value = cursor
.slice(digit_start, cursor.pos())
.parse::<i32>()
.map_err(|err| {
Error::general(err.to_string(), Position::new(digit_start, cursor.pos()))
})?;
.map_err(|err| TemporalError::syntax().with_message(err.to_string()))?;
match cursor.peek() {
Some(ch) if is_year_designator(ch) => {
if previous_unit > DateUnit::Year {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.years = value;
previous_unit = DateUnit::Year;
}
Some(ch) if is_month_designator(ch) => {
if previous_unit > DateUnit::Month {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.months = value;
previous_unit = DateUnit::Month;
}
Some(ch) if is_week_designator(ch) => {
if previous_unit > DateUnit::Week {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.weeks = value;
previous_unit = DateUnit::Week;
}
Some(ch) if is_day_designator(ch) => {
if previous_unit > DateUnit::Day {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.days = value;
previous_unit = DateUnit::Day;
}
Some(_) | None => return Err(Error::AbruptEnd),
Some(_) | None => return Err(TemporalError::abrupt_end()),
}
cursor.advance();
@ -187,14 +178,13 @@ enum TimeUnit {
Second,
}
pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDuration> {
pub(crate) fn parse_time_duration(cursor: &mut Cursor) -> TemporalResult<TimeDuration> {
let mut time = TimeDuration::default();
if !cursor.check_or(false, |ch| ch.is_ascii()) {
return Err(Error::general(
"No time values provided after TimeDesignator.",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("No time values provided after TimeDesignator.")
);
}
let mut previous_unit = TimeUnit::None;
@ -209,9 +199,7 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDur
let value = cursor
.slice(digit_start, cursor.pos())
.parse::<i32>()
.map_err(|err| {
Error::general(err.to_string(), Position::new(digit_start, cursor.pos()))
})?;
.map_err(|err| TemporalError::syntax().with_message(err.to_string()))?;
let fraction = if cursor.check_or(false, is_decimal_separator) {
fraction_present = true;
@ -223,10 +211,9 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDur
match cursor.peek() {
Some(ch) if is_hour_designator(ch) => {
if previous_unit > TimeUnit::Hour {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
time.hours = value;
time.fhours = fraction;
@ -234,10 +221,9 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDur
}
Some(ch) if is_minute_designator(ch) => {
if previous_unit > TimeUnit::Minute {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
time.minutes = value;
time.fminutes = fraction;
@ -245,26 +231,23 @@ pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDur
}
Some(ch) if is_second_designator(ch) => {
if previous_unit > TimeUnit::Second {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
time.seconds = value;
time.fseconds = fraction;
previous_unit = TimeUnit::Second;
}
Some(_) | None => return Err(Error::AbruptEnd),
Some(_) | None => return Err(TemporalError::abrupt_end()),
}
cursor.advance();
if fraction_present {
if cursor.check_or(false, |ch| ch.is_ascii_digit()) {
return Err(Error::general(
"Invalid TimeDuration continuation after FractionPart.",
Position::new(1, cursor.pos()),
));
return Err(TemporalError::syntax()
.with_message("Invalid TimeDuration continuation after FractionPart."));
}
break;

0
boa_parser/src/temporal/grammar.rs → boa_temporal/src/parser/grammar.rs

151
boa_parser/src/temporal/mod.rs → boa_temporal/src/parser/mod.rs

@ -1,25 +1,40 @@
//! Implementation of Iso8601 grammar lexing/parsing
use crate::error::ParseResult;
use crate::TemporalResult;
use date_time::DateRecord;
use nodes::{IsoDate, IsoDateTime, IsoTime, TimeZone};
use time::TimeSpec;
mod annotations;
mod date_time;
mod duration;
pub(crate) mod date_time;
pub(crate) mod duration;
mod grammar;
mod nodes;
mod time;
mod time_zone;
use boa_ast::temporal::{IsoDate, IsoDateTime, IsoDuration, IsoTime, TimeZone};
pub(crate) mod time_zone;
use date_time::DateRecord;
use time::TimeSpec;
use self::date_time::DateTimeFlags;
#[cfg(feature = "experimental")]
#[cfg(test)]
mod tests;
// TODO: optimize where possible.
/// A utility function for parsing a `DateTime` string
pub(crate) fn parse_date_time(target: &str) -> TemporalResult<IsoParseRecord> {
date_time::parse_annotated_date_time(DateTimeFlags::empty(), &mut Cursor::new(target))
}
/// A utility function for parsing an `Instant` string
#[allow(unused)]
pub(crate) fn parse_instant(target: &str) -> TemporalResult<IsoParseRecord> {
date_time::parse_annotated_date_time(
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ,
&mut Cursor::new(target),
)
}
/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions.
///
/// `IsoParseRecord` is converted into the ISO AST Nodes.
@ -35,40 +50,7 @@ pub(crate) struct IsoParseRecord {
pub(crate) calendar: Option<String>,
}
/// Parse a [`TemporalDateTimeString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString
#[derive(Debug, Clone, Copy)]
pub struct TemporalDateTimeString;
impl TemporalDateTimeString {
/// Parses a targeted string as a `DateTime`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(zoned: bool, cursor: &mut IsoCursor) -> ParseResult<IsoDateTime> {
let parse_record = date_time::parse_annotated_date_time(zoned, false, false, cursor)?;
let date = IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
};
let time = parse_record.time.map_or_else(IsoTime::default, |time| {
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction)
});
Ok(IsoDateTime {
date,
time,
tz: parse_record.tz,
})
}
}
// TODO: Phase out the below and integrate parsing with Temporal components.
/// Parse a [`TemporalTimeZoneString`][proposal].
///
@ -83,7 +65,7 @@ impl TemporalTimeZoneString {
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
pub fn parse(cursor: &mut Cursor) -> TemporalResult<TimeZone> {
time_zone::parse_time_zone(cursor)
}
}
@ -101,7 +83,8 @@ impl TemporalYearMonthString {
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDate> {
pub fn parse(cursor: &mut Cursor) -> TemporalResult<IsoDate> {
// TODO: Remove peek in favor of AnnotatedDateTime flag.
if date_time::peek_year_month(cursor)? {
let ym = date_time::parse_year_month(cursor)?;
@ -120,7 +103,7 @@ impl TemporalYearMonthString {
});
}
let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?;
let parse_record = date_time::parse_annotated_date_time(DateTimeFlags::empty(), cursor)?;
Ok(IsoDate {
year: parse_record.date.year,
@ -144,7 +127,8 @@ impl TemporalMonthDayString {
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDate> {
pub fn parse(cursor: &mut Cursor) -> TemporalResult<IsoDate> {
// TODO: Remove peek in favor of AnnotatedDateTime flag.
if date_time::peek_month_day(cursor)? {
let md = date_time::parse_month_day(cursor)?;
@ -163,7 +147,7 @@ impl TemporalMonthDayString {
});
}
let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?;
let parse_record = date_time::parse_annotated_date_time(DateTimeFlags::empty(), cursor)?;
Ok(IsoDate {
year: parse_record.date.year,
@ -187,8 +171,11 @@ impl TemporalInstantString {
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDateTime> {
let parse_record = date_time::parse_annotated_date_time(false, true, true, cursor)?;
pub fn parse(cursor: &mut Cursor) -> TemporalResult<IsoDateTime> {
let parse_record = date_time::parse_annotated_date_time(
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ,
cursor,
)?;
let date = IsoDate {
year: parse_record.date.year,
@ -209,74 +196,16 @@ impl TemporalInstantString {
}
}
// TODO: implement TemporalTimeString.
/// Parser for a [`TemporalDurationString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDurationString
#[derive(Debug, Clone, Copy)]
pub struct TemporalDurationString;
impl TemporalDurationString {
/// Parses a targeted string as a `Duration`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDuration> {
let parse_record = duration::parse_duration(cursor)?;
let minutes = if parse_record.time.fhours > 0.0 {
parse_record.time.fhours * 60.0
} else {
f64::from(parse_record.time.minutes)
};
let seconds = if parse_record.time.fminutes > 0.0 {
parse_record.time.fminutes * 60.0
} else if parse_record.time.seconds > 0 {
f64::from(parse_record.time.seconds)
} else {
minutes.rem_euclid(1.0) * 60.0
};
let milliseconds = if parse_record.time.fseconds > 0.0 {
parse_record.time.fseconds * 1000.0
} else {
seconds.rem_euclid(1.0) * 1000.0
};
let micro = milliseconds.rem_euclid(1.0) * 1000.0;
let nano = micro.rem_euclid(1.0) * 1000.0;
let sign = if parse_record.sign { 1 } else { -1 };
Ok(IsoDuration {
years: parse_record.date.years * sign,
months: parse_record.date.months * sign,
weeks: parse_record.date.weeks * sign,
days: parse_record.date.days * sign,
hours: parse_record.time.hours * sign,
minutes: minutes.floor() * f64::from(sign),
seconds: seconds.floor() * f64::from(sign),
milliseconds: milliseconds.floor() * f64::from(sign),
microseconds: micro.floor() * f64::from(sign),
nanoseconds: nano.floor() * f64::from(sign),
})
}
}
// ==== Mini cursor implementation for Iso8601 targets ====
/// `IsoCursor` is a small cursor implementation for parsing Iso8601 grammar.
/// `Cursor` is a small cursor implementation for parsing Iso8601 grammar.
#[derive(Debug)]
pub struct IsoCursor {
pub struct Cursor {
pos: u32,
source: Vec<char>,
}
impl IsoCursor {
impl Cursor {
/// Create a new cursor from a source `String` value.
#[must_use]
pub fn new(source: &str) -> Self {

33
boa_ast/src/temporal/mod.rs → boa_temporal/src/parser/nodes.rs

@ -1,5 +1,7 @@
//! AST nodes for Temporal's implementation of ISO8601 grammar.
// TODO: Slowly remove the below nodes in favor of Temporal components.
/// An ISO Date Node consisting of non-validated date fields and calendar value.
#[derive(Default, Debug)]
pub struct IsoDate {
@ -37,9 +39,9 @@ impl IsoTime {
pub fn from_components(hour: u8, minute: u8, second: u8, fraction: f64) -> Self {
// Note: Precision on nanoseconds drifts, so opting for round over floor or ceil for now.
// e.g. 0.329402834 becomes 329.402833.999
let millisecond = fraction * 1000.0;
let micros = millisecond.rem_euclid(1.0) * 1000.0;
let nanos = micros.rem_euclid(1.0) * 1000.0;
let millisecond = fraction * 1000f64;
let micros = millisecond.rem_euclid(1f64) * 1000f64;
let nanos = micros.rem_euclid(1f64) * 1000f64;
Self {
hour,
@ -86,28 +88,3 @@ pub struct UTCOffset {
/// Any sub second components of the `UTCOffset`
pub fraction: f64,
}
/// An `IsoDuration` Node output by the ISO parser.
#[derive(Debug, Default, Clone, Copy)]
pub struct IsoDuration {
/// Years value.
pub years: i32,
/// Months value.
pub months: i32,
/// Weeks value.
pub weeks: i32,
/// Days value.
pub days: i32,
/// Hours value.
pub hours: i32,
/// Minutes value.
pub minutes: f64,
/// Seconds value.
pub seconds: f64,
/// Milliseconds value.
pub milliseconds: f64,
/// Microseconds value.
pub microseconds: f64,
/// Nanoseconds value.
pub nanoseconds: f64,
}

92
boa_parser/src/temporal/tests.rs → boa_temporal/src/parser/tests.rs

@ -1,6 +1,12 @@
use super::{
IsoCursor, TemporalDateTimeString, TemporalDurationString, TemporalInstantString,
TemporalMonthDayString, TemporalYearMonthString,
use std::str::FromStr;
use crate::{
datetime::DateTime,
duration::Duration,
parser::{
parse_date_time, Cursor, TemporalInstantString, TemporalMonthDayString,
TemporalYearMonthString,
},
};
#[test]
@ -8,17 +14,19 @@ fn temporal_parser_basic() {
let basic = "20201108";
let basic_separated = "2020-11-08";
let basic_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap();
let basic_result = basic.parse::<DateTime>().unwrap();
let sep_result =
TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic_separated)).unwrap();
let sep_result = basic_separated.parse::<DateTime>().unwrap();
assert_eq!(basic_result.date.year, 2020);
assert_eq!(basic_result.date.month, 11);
assert_eq!(basic_result.date.day, 8);
assert_eq!(basic_result.date.year, sep_result.date.year);
assert_eq!(basic_result.date.month, sep_result.date.month);
assert_eq!(basic_result.date.day, sep_result.date.day);
assert_eq!(basic_result.iso_date().year(), 2020);
assert_eq!(basic_result.iso_date().month(), 11);
assert_eq!(basic_result.iso_date().day(), 8);
assert_eq!(basic_result.iso_date().year(), sep_result.iso_date().year());
assert_eq!(
basic_result.iso_date().month(),
sep_result.iso_date().month()
);
assert_eq!(basic_result.iso_date().day(), sep_result.iso_date().day());
}
#[test]
@ -28,9 +36,9 @@ fn temporal_date_time_max() {
let date_time =
"+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]";
let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(date_time)).unwrap();
let result = date_time.parse::<DateTime>().unwrap();
let time_results = &result.time;
let time_results = result.iso_time();
assert_eq!(time_results.hour, 12);
assert_eq!(time_results.minute, 28);
@ -38,17 +46,6 @@ fn temporal_date_time_max() {
assert_eq!(time_results.millisecond, 329);
assert_eq!(time_results.microsecond, 402);
assert_eq!(time_results.nanosecond, 834);
let tz = &result.tz.unwrap();
// OffsetSubMinute is Empty when TimeZoneIdentifier is present.
assert!(&tz.offset.is_none());
let tz_name = &tz.name.clone().unwrap();
assert_eq!(tz_name, "America/Argentina/ComodRivadavia");
assert_eq!(&result.date.calendar, &Some("iso8601".to_string()));
}
#[test]
@ -56,10 +53,10 @@ fn temporal_year_parsing() {
let long = "+002020-11-08";
let bad_year = "-000000-11-08";
let result_good = TemporalDateTimeString::parse(false, &mut IsoCursor::new(long)).unwrap();
assert_eq!(result_good.date.year, 2020);
let result_good = long.parse::<DateTime>().unwrap();
assert_eq!(result_good.iso_date().year(), 2020);
let err_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(bad_year));
let err_result = bad_year.parse::<DateTime>();
assert!(err_result.is_err());
}
@ -68,19 +65,19 @@ fn temporal_annotated_date_time() {
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)).unwrap();
let result = parse_date_time(basic).unwrap();
let tz = &result.tz.unwrap().name.unwrap();
assert_eq!(tz, "America/Argentina/ComodRivadavia");
assert_eq!(&result.date.calendar, &Some("iso8601".to_string()));
assert_eq!(&result.calendar, &Some("iso8601".to_string()));
let omit_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(omitted)).unwrap();
let omit_result = parse_date_time(omitted).unwrap();
assert!(&omit_result.tz.is_none());
assert_eq!(&omit_result.date.calendar, &Some("iso8601".to_string()));
assert_eq!(&omit_result.calendar, &Some("iso8601".to_string()));
}
#[test]
@ -93,7 +90,7 @@ fn temporal_year_month() {
];
for ym in possible_year_months {
let result = TemporalYearMonthString::parse(&mut IsoCursor::new(ym)).unwrap();
let result = TemporalYearMonthString::parse(&mut Cursor::new(ym)).unwrap();
assert_eq!(result.year, 2020);
assert_eq!(result.month, 11);
@ -109,7 +106,7 @@ fn temporal_month_day() {
let possible_month_day = ["11-07", "1107[+04:00]", "--11-07", "--1107[+04:00]"];
for md in possible_month_day {
let result = TemporalMonthDayString::parse(&mut IsoCursor::new(md)).unwrap();
let result = TemporalMonthDayString::parse(&mut Cursor::new(md)).unwrap();
assert_eq!(result.month, 11);
assert_eq!(result.day, 7);
@ -125,7 +122,7 @@ fn temporal_invalid_annotations() {
];
for invalid in invalid_annotations {
let err_result = TemporalMonthDayString::parse(&mut IsoCursor::new(invalid));
let err_result = TemporalMonthDayString::parse(&mut Cursor::new(invalid));
assert!(err_result.is_err());
}
}
@ -139,13 +136,14 @@ fn temporal_valid_instant_strings() {
];
for test in instants {
let result = TemporalInstantString::parse(&mut IsoCursor::new(test));
let result = TemporalInstantString::parse(&mut Cursor::new(test));
assert!(result.is_ok());
}
}
#[test]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::float_cmp)]
fn temporal_duration_parsing() {
let durations = [
"p1y1m1dt1h1m1s",
@ -155,23 +153,21 @@ fn temporal_duration_parsing() {
];
for dur in durations {
let ok_result = TemporalDurationString::parse(&mut IsoCursor::new(dur));
let ok_result = Duration::from_str(dur);
assert!(ok_result.is_ok());
}
let sub = durations[2];
let sub_second = TemporalDurationString::parse(&mut IsoCursor::new(sub)).unwrap();
let sub_second = durations[2].parse::<Duration>().unwrap();
assert_eq!(sub_second.milliseconds, -123.0);
assert_eq!(sub_second.microseconds, -456.0);
assert_eq!(sub_second.nanoseconds, -789.0);
assert_eq!(sub_second.time().milliseconds(), -123.0);
assert_eq!(sub_second.time().microseconds(), -456.0);
assert_eq!(sub_second.time().nanoseconds(), -789.0);
let dur = durations[3];
let test_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)).unwrap();
let test_result = durations[3].parse::<Duration>().unwrap();
assert_eq!(test_result.years, -1);
assert_eq!(test_result.weeks, -3);
assert_eq!(test_result.minutes, -30.0);
assert_eq!(test_result.date().years(), -1f64);
assert_eq!(test_result.date().weeks(), -3f64);
assert_eq!(test_result.time().minutes(), -30.0);
}
#[test]
@ -184,7 +180,7 @@ fn temporal_invalid_durations() {
];
for test in invalids {
let err = TemporalDurationString::parse(&mut IsoCursor::new(test));
let err = test.parse::<Duration>();
assert!(err.is_err());
}
}

48
boa_parser/src/temporal/time.rs → boa_temporal/src/parser/time.rs

@ -2,12 +2,9 @@
use super::{
grammar::{is_decimal_separator, is_time_separator},
IsoCursor,
};
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
Cursor,
};
use crate::{TemporalError, TemporalResult};
/// Parsed Time info
#[derive(Debug, Default, Clone, Copy)]
@ -22,10 +19,8 @@ pub(crate) struct TimeSpec {
pub(crate) fraction: f64,
}
use boa_ast::Position;
/// Parse `TimeSpec`
pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult<TimeSpec> {
pub(crate) fn parse_time_spec(cursor: &mut Cursor) -> TemporalResult<TimeSpec> {
let hour = parse_hour(cursor)?;
let mut separator = false;
@ -50,9 +45,7 @@ pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult<TimeSpec> {
if separator && is_time_separator {
cursor.advance();
} else if is_time_separator {
return Err(
LexError::syntax("Invalid TimeSeparator", Position::new(1, cursor.pos())).into(),
);
return Err(TemporalError::syntax().with_message("Invalid TimeSeparator"));
}
} else {
return Ok(TimeSpec {
@ -79,17 +72,13 @@ pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult<TimeSpec> {
})
}
pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult<u8> {
pub(crate) fn parse_hour(cursor: &mut Cursor) -> TemporalResult<u8> {
let hour_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<u8>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
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),
)
.into());
return Err(TemporalError::syntax().with_message("Hour must be in a range of 0-23"));
}
cursor.advance_n(2);
Ok(hour_value)
@ -97,19 +86,15 @@ pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult<u8> {
// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`.
/// Parse `MinuteSecond`
pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> ParseResult<u8> {
pub(crate) fn parse_minute_second(cursor: &mut Cursor, inclusive: bool) -> TemporalResult<u8> {
let min_sec_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<u8>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
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),
)
.into());
return Err(TemporalError::syntax().with_message("MinuteSecond must be in a range of 0-59"));
}
cursor.advance_n(2);
@ -120,27 +105,26 @@ pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> Pa
///
/// This is primarily used in ISO8601 to add percision past
/// a second.
pub(crate) fn parse_fraction(cursor: &mut IsoCursor) -> ParseResult<f64> {
pub(crate) fn parse_fraction(cursor: &mut Cursor) -> TemporalResult<f64> {
// Decimal is skipped by next call.
let mut fraction_components = Vec::from(['.']);
while let Some(ch) = cursor.next() {
if !ch.is_ascii_digit() {
if fraction_components.len() > 10 {
return Err(Error::general(
"Fraction exceeds 9 DecimalDigits",
Position::new(1, cursor.pos() - 1),
));
return Err(
TemporalError::syntax().with_message("Fraction exceeds 9 DecimalDigits")
);
}
let fraction_value = fraction_components
.iter()
.collect::<String>()
.parse::<f64>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() - 1)))?;
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
return Ok(fraction_value);
}
fraction_components.push(ch);
}
Err(Error::AbruptEnd)
Err(TemporalError::abrupt_end())
}

79
boa_parser/src/temporal/time_zone.rs → boa_temporal/src/parser/time_zone.rs

@ -7,18 +7,11 @@ use super::{
is_decimal_separator, is_sign, is_time_separator, is_tz_char, is_tz_leading_char,
is_tz_name_separator, is_utc_designator,
},
nodes::{TimeZone, UTCOffset},
time::{parse_fraction, parse_hour, parse_minute_second},
IsoCursor,
};
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
};
use boa_ast::{
temporal::{TimeZone, UTCOffset},
Position,
Cursor,
};
use crate::{TemporalError, TemporalResult};
/// A `TimeZoneAnnotation`.
#[derive(Debug, Clone)]
@ -33,14 +26,14 @@ pub(crate) struct TimeZoneAnnotation {
// ==== Time Zone Annotation Parsing ====
pub(crate) fn parse_ambiguous_tz_annotation(
cursor: &mut IsoCursor,
) -> ParseResult<Option<TimeZoneAnnotation>> {
cursor: &mut Cursor,
) -> TemporalResult<Option<TimeZoneAnnotation>> {
// Peek position + 1 to check for critical flag.
let mut current_peek = 1;
let critical = cursor
.peek_n(current_peek)
.map(is_critical_flag)
.ok_or_else(|| Error::AbruptEnd)?;
.ok_or_else(TemporalError::abrupt_end)?;
// Advance cursor if critical flag present.
if critical {
@ -49,7 +42,7 @@ pub(crate) fn parse_ambiguous_tz_annotation(
let leading_char = cursor
.peek_n(current_peek)
.ok_or_else(|| Error::AbruptEnd)?;
.ok_or_else(TemporalError::abrupt_end)?;
if is_tz_leading_char(leading_char) || is_sign(leading_char) {
// Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`.
@ -64,16 +57,12 @@ pub(crate) fn parse_ambiguous_tz_annotation(
{
return Ok(None);
} else if is_annotation_close(ch) {
return Err(LexError::syntax(
"Invalid Annotation",
Position::new(1, peek_pos + 1),
)
.into());
return Err(TemporalError::syntax().with_message("Invalid Annotation"));
}
peek_pos += 1;
}
return Err(Error::AbruptEnd);
return Err(TemporalError::abrupt_end());
}
let tz = parse_tz_annotation(cursor)?;
return Ok(Some(tz));
@ -83,16 +72,13 @@ pub(crate) fn parse_ambiguous_tz_annotation(
return Ok(None);
};
Err(Error::lex(LexError::syntax(
"Unexpected character in ambiguous annotation.",
Position::new(1, cursor.pos() + 1),
)))
Err(TemporalError::syntax().with_message("Unexpected character in ambiguous annotation."))
}
fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult<TimeZoneAnnotation> {
fn parse_tz_annotation(cursor: &mut Cursor) -> TemporalResult<TimeZoneAnnotation> {
debug_assert!(is_annotation_open(cursor.peek().expect("annotation start")));
let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?;
let potential_critical = cursor.next().ok_or_else(TemporalError::abrupt_end)?;
let critical = is_critical_flag(potential_critical);
if critical {
@ -102,11 +88,7 @@ fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult<TimeZoneAnnotation
let tz = parse_time_zone(cursor)?;
if !cursor.check_or(false, is_annotation_close) {
return Err(LexError::syntax(
"Invalid TimeZoneAnnotation.",
Position::new(1, cursor.pos() + 1),
)
.into());
return Err(TemporalError::syntax().with_message("Invalid TimeZoneAnnotation."));
}
cursor.advance();
@ -117,10 +99,10 @@ fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult<TimeZoneAnnotation
/// Parses the [`TimeZoneIdentifier`][tz] node.
///
/// [tz]: https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier
pub(crate) fn parse_time_zone(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
pub(crate) fn parse_time_zone(cursor: &mut Cursor) -> TemporalResult<TimeZone> {
let is_iana = cursor
.check(is_tz_leading_char)
.ok_or_else(|| Error::AbruptEnd)?;
.ok_or_else(TemporalError::abrupt_end)?;
let is_offset = cursor.check_or(false, is_sign);
if is_iana {
@ -133,24 +115,17 @@ pub(crate) fn parse_time_zone(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
});
}
Err(LexError::syntax(
"Invalid leading character for a TimeZoneIdentifier",
Position::new(1, cursor.pos() + 1),
)
.into())
Err(TemporalError::syntax().with_message("Invalid leading character for a TimeZoneIdentifier"))
}
/// Parse a `TimeZoneIANAName` Parse Node
fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
fn parse_tz_iana_name(cursor: &mut Cursor) -> TemporalResult<TimeZone> {
let tz_name_start = cursor.pos();
while let Some(potential_value_char) = cursor.next() {
if is_tz_name_separator(potential_value_char) {
if !cursor.peek_n(1).map_or(false, is_tz_char) {
return Err(LexError::syntax(
"Missing TimeZoneIANANameComponent after '/'",
Position::new(1, cursor.pos() + 2),
)
.into());
return Err(TemporalError::syntax()
.with_message("Missing TimeZoneIANANameComponent after '/'"));
}
continue;
}
@ -164,13 +139,13 @@ fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
}
}
Err(Error::AbruptEnd)
Err(TemporalError::abrupt_end())
}
// ==== Utc Offset Parsing ====
/// Parse a full precision `UtcOffset`
pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
pub(crate) fn parse_date_time_utc(cursor: &mut Cursor) -> TemporalResult<TimeZone> {
if cursor.check_or(false, is_utc_designator) {
cursor.advance();
return Ok(TimeZone {
@ -185,11 +160,7 @@ pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult<TimeZon
if cursor.check_or(false, is_time_separator) {
if !separated {
return Err(LexError::syntax(
"Unexpected TimeSeparator",
Position::new(1, cursor.pos()),
)
.into());
return Err(TemporalError::syntax().with_message("Unexpected TimeSeparator"));
}
cursor.advance();
}
@ -220,7 +191,7 @@ pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult<TimeZon
}
/// Parse an `UtcOffsetMinutePrecision` node
pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> ParseResult<UTCOffset> {
pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut Cursor) -> TemporalResult<UTCOffset> {
let sign = if let Some(ch) = cursor.next() {
if ch == '+' {
1_i8
@ -228,14 +199,14 @@ pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> Parse
-1_i8
}
} else {
return Err(Error::AbruptEnd);
return Err(TemporalError::abrupt_end());
};
let hour = parse_hour(cursor)?;
// If at the end of the utc, then return.
if cursor
.check(|ch| !(ch.is_ascii_digit() || is_time_separator(ch)))
.ok_or_else(|| Error::AbruptEnd)?
.ok_or_else(TemporalError::abrupt_end)?
{
return Ok(UTCOffset {
sign,
Loading…
Cancel
Save