LibJS: Align ISO 8601 grammar with annotations from IXDTF

This is a normative change in the Temporal spec.

See: https://github.com/tc39/proposal-temporal/commit/c64b844
This commit is contained in:
Luke Wilde 2022-11-02 18:26:46 +00:00 committed by Linus Groh
parent 0f2b4d0aac
commit 192aa75279
Notes: sideshowbarker 2024-07-17 11:29:41 +09:00
6 changed files with 245 additions and 93 deletions

View file

@ -280,6 +280,7 @@
M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \
M(TemporalPropertyMustBePositiveInteger, "Property must be a positive integer") \
M(TemporalTimeZoneOffsetStringMismatch, "Time zone offset string mismatch: '{}' is not equal to '{}'") \
M(TemporalUnknownCriticalAnnotation, "Unknown annotation key in critical annotation: '{}'") \
M(TemporalZonedDateTimeRoundZeroOrNegativeLengthDay, "Cannot round a ZonedDateTime in a calendar or time zone that has zero or " \
"negative length days") \
M(ThisHasNotBeenInitialized, "|this| has not been initialized") \

View file

@ -1215,7 +1215,7 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, StringView iso_string
// 13.28 ParseISODateTime ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime
ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& parse_result)
{
// 4. Let each of year, month, day, hour, minute, second, fSeconds, and calendar be the source text matched by the respective DateYear, DateMonth, DateDay, TimeHour, TimeMinute, TimeSecond, TimeFraction, and CalendarName Parse Node contained within parseResult, or an empty sequence of code points if not present.
// 4. Let each of year, month, day, hour, minute, second, and fSeconds be the source text matched by the respective DateYear, DateMonth, DateDay, TimeHour, TimeMinute, TimeSecond, and TimeFraction Parse Node contained within parseResult, or an empty sequence of code points if not present.
auto year = parse_result.date_year;
auto month = parse_result.date_month;
auto day = parse_result.date_day;
@ -1223,7 +1223,6 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
auto minute = parse_result.time_minute;
auto second = parse_result.time_second;
auto f_seconds = parse_result.time_fraction;
auto calendar = parse_result.calendar_name;
// 5. If the first code point of year is U+2212 (MINUS SIGN), replace the first code point with U+002D (HYPHEN-MINUS).
Optional<String> normalized_year;
@ -1342,22 +1341,35 @@ ThrowCompletionOr<ISODateTime> parse_iso_date_time(VM& vm, ParseResult const& pa
}
}
Optional<String> calendar_val;
// 23. Let calendar be undefined.
Optional<String> calendar;
// 23. If calendar is empty, then
if (!calendar.has_value()) {
// a. Let calendarVal be undefined.
calendar_val = {};
}
// 24. Else,
else {
// a. Let calendarVal be CodePointsToString(calendar).
// NOTE: This turns the StringView into a String.
calendar_val = *calendar;
// 24. For each Annotation Parse Node annotation contained within parseResult, do
for (auto const& annotation : parse_result.annotations) {
// a. Let key be the source text matched by the AnnotationKey Parse Node contained within annotation.
auto const& key = annotation.key;
// b. If CodePointsToString(key) is "u-ca", then
if (key == "u-ca"sv) {
// i. If calendar is undefined, then
if (!calendar.has_value()) {
// 1. Let value be the source text matched by the AnnotationValue Parse Node contained within annotation.
auto const& value = annotation.value;
// 2. Let calendar be CodePointsToString(value).
calendar = value;
}
}
// c. Else,
else {
// i. If annotation contains an AnnotationCriticalFlag Parse Node, throw a RangeError exception.
if (annotation.critical)
return vm.throw_completion<RangeError>(ErrorType::TemporalUnknownCriticalAnnotation, key);
}
}
// 25. Return the Record { [[Year]]: yearMV, [[Month]]: monthMV, [[Day]]: dayMV, [[Hour]]: hourMV, [[Minute]]: minuteMV, [[Second]]: secondMV, [[Millisecond]]: millisecondMV, [[Microsecond]]: microsecondMV, [[Nanosecond]]: nanosecondMV, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendarVal, }.
return ISODateTime { .year = year_mv, .month = month_mv, .day = day_mv, .hour = hour_mv, .minute = minute_mv, .second = second_mv, .millisecond = millisecond_mv, .microsecond = microsecond_mv, .nanosecond = nanosecond_mv, .time_zone = move(time_zone_result), .calendar = move(calendar_val) };
// 25. Return the Record { [[Year]]: yearMV, [[Month]]: monthMV, [[Day]]: dayMV, [[Hour]]: hourMV, [[Minute]]: minuteMV, [[Second]]: secondMV, [[Millisecond]]: millisecondMV, [[Microsecond]]: microsecondMV, [[Nanosecond]]: nanosecondMV, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendar }.
return ISODateTime { .year = year_mv, .month = month_mv, .day = day_mv, .hour = hour_mv, .minute = minute_mv, .second = second_mv, .millisecond = millisecond_mv, .microsecond = microsecond_mv, .nanosecond = nanosecond_mv, .time_zone = move(time_zone_result), .calendar = move(calendar) };
}
// 13.29 ParseTemporalInstantString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalinstantstring
@ -1419,8 +1431,8 @@ ThrowCompletionOr<String> parse_temporal_calendar_string(VM& vm, String const& i
}
// 3. Else,
else {
// a. Set parseResult to ParseText(StringToCodePoints(isoString), CalendarName).
auto parse_result = parse_iso8601(Production::CalendarName, iso_string);
// a. Set parseResult to ParseText(StringToCodePoints(isoString), AnnotationValue).
auto parse_result = parse_iso8601(Production::AnnotationValue, iso_string);
// b. If parseResult is a List of errors, throw a RangeError exception.
if (!parse_result.has_value())

View file

@ -269,6 +269,14 @@ bool ISO8601Parser::parse_utc_designator()
return true;
}
// https://tc39.es/proposal-temporal/#prod-AnnotationCriticalFlag
bool ISO8601Parser::parse_annotation_critical_flag()
{
// AnnotationCriticalFlag :
// !
return m_state.lexer.consume_specific('!');
}
// https://tc39.es/proposal-temporal/#prod-DateYear
bool ISO8601Parser::parse_date_year()
{
@ -822,10 +830,11 @@ bool ISO8601Parser::parse_time_zone_identifier()
bool ISO8601Parser::parse_time_zone_bracketed_annotation()
{
// TimeZoneBracketedAnnotation :
// [ TimeZoneIdentifier ]
// [ AnnotationCriticalFlag[opt] TimeZoneIdentifier ]
StateTransaction transaction { *this };
if (!m_state.lexer.consume_specific('['))
return false;
(void)parse_annotation_critical_flag();
if (!parse_time_zone_identifier())
return false;
if (!m_state.lexer.consume_specific(']'))
@ -876,53 +885,166 @@ bool ISO8601Parser::parse_time_zone()
return true;
}
// https://tc39.es/proposal-temporal/#prod-CalendarName
bool ISO8601Parser::parse_calendar_name()
// https://tc39.es/proposal-temporal/#prod-AKeyLeadingChar
bool ISO8601Parser::parse_a_key_leading_char()
{
// CalChar :
// AKeyLeadingChar :
// LowercaseAlpha
// _
if (m_state.lexer.next_is(is_ascii_lower_alpha)) {
m_state.lexer.consume();
return true;
}
return m_state.lexer.consume_specific('_');
}
// https://tc39.es/proposal-temporal/#prod-AKeyChar
bool ISO8601Parser::parse_a_key_char()
{
// AKeyChar :
// AKeyLeadingChar
// DecimalDigit
// -
if (parse_a_key_leading_char())
return true;
if (parse_decimal_digit())
return true;
return m_state.lexer.consume_specific('-');
}
// https://tc39.es/proposal-temporal/#prod-AValChar
bool ISO8601Parser::parse_a_val_char()
{
// AValChar :
// Alpha
// DecimalDigit
// CalendarNameComponent :
// CalChar CalChar CalChar CalChar[opt] CalChar[opt] CalChar[opt] CalChar[opt] CalChar[opt]
// CalendarNameTail :
// CalendarNameComponent
// CalendarNameComponent - CalendarNameTail
// CalendarName :
// CalendarNameTail
auto parse_calendar_name_component = [&] {
for (size_t i = 0; i < 8; ++i) {
if (!m_state.lexer.next_is(is_ascii_alphanumeric))
return i > 2;
m_state.lexer.consume();
}
if (m_state.lexer.next_is(is_ascii_alpha)) {
m_state.lexer.consume();
return true;
};
}
return parse_decimal_digit();
}
// https://tc39.es/proposal-temporal/#prod-AnnotationKeyTail
bool ISO8601Parser::parse_annotation_key_tail()
{
// AnnotationKeyTail :
// AKeyChar AnnotationKeyTail[opt]
if (!parse_a_key_char())
return false;
// This is implemented without recursion to prevent stack overflow with annotation key tails that have many characters.
while (parse_a_key_char())
;
return true;
}
// https://tc39.es/proposal-temporal/#prod-AnnotationKey
bool ISO8601Parser::parse_annotation_key()
{
// AnnotationKey :
// AKeyLeadingChar AnnotationKeyTail[opt]
StateTransaction transaction { *this };
do {
if (!parse_calendar_name_component())
return false;
} while (m_state.lexer.consume_specific('-'));
m_state.parse_result.calendar_name = transaction.parsed_string_view();
if (!parse_a_key_leading_char()) {
m_state.parse_result.annotation_key = Optional<StringView> {};
return false;
}
(void)parse_annotation_key_tail();
m_state.parse_result.annotation_key = transaction.parsed_string_view();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-Calendar
bool ISO8601Parser::parse_calendar()
// https://tc39.es/proposal-temporal/#prod-AnnotationValueComponent
bool ISO8601Parser::parse_annotation_value_component()
{
// Calendar :
// [u-ca= CalendarName ]
// AnnotationValueComponent :
// AValChar AnnotationValueComponent[opt]
if (!parse_a_val_char())
return false;
// This is implemented without recursion to prevent stack overflow with annotation value components that have many characters.
while (parse_a_val_char())
;
return true;
}
// https://tc39.es/proposal-temporal/#prod-AnnotationValueTail
bool ISO8601Parser::parse_annotation_value_tail()
{
// AnnotationValueTail :
// AnnotationValueComponent
// AnnotationValueComponent - AnnotationValueTail
// This is implemented without recursion to prevent stack overflow with annotation values that have many dashes.
for (;;) {
if (!parse_annotation_value_component())
return false;
if (!m_state.lexer.consume_specific('-'))
break;
}
return true;
}
// https://tc39.es/proposal-temporal/#prod-AnnotationValue
bool ISO8601Parser::parse_annotation_value()
{
// AnnotationValue :
// AnnotationValueTail
StateTransaction transaction { *this };
if (!m_state.lexer.consume_specific("[u-ca="sv))
if (!parse_annotation_value_tail()) {
m_state.parse_result.annotation_value = Optional<StringView> {};
return false;
if (!parse_calendar_name())
}
m_state.parse_result.annotation_value = transaction.parsed_string_view();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-Annotation
bool ISO8601Parser::parse_annotation()
{
// Annotation :
// [ AnnotationCriticalFlag[opt] AnnotationKey = AnnotationValue ]
StateTransaction transaction { *this };
if (!m_state.lexer.consume_specific('['))
return false;
Annotation annotation;
annotation.critical = parse_annotation_critical_flag();
if (!parse_annotation_key())
return false;
annotation.key = m_state.parse_result.annotation_key.value();
if (!m_state.lexer.consume_specific('='))
return false;
if (!parse_annotation_value())
return false;
annotation.value = m_state.parse_result.annotation_value.value();
if (!m_state.lexer.consume_specific(']'))
return false;
m_state.parse_result.annotations.append(annotation);
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-Annotations
bool ISO8601Parser::parse_annotations()
{
// Annotations :
// Annotation Annotations[opt]
if (!parse_annotation())
return false;
// This is implemented without recursion to prevent stack overflow with ISO strings that have many annotations.
while (parse_annotation())
;
return true;
}
// https://tc39.es/proposal-temporal/#prod-TimeSpec
bool ISO8601Parser::parse_time_spec()
{
@ -995,17 +1117,17 @@ bool ISO8601Parser::parse_date_time()
return true;
}
// https://tc39.es/proposal-temporal/#prod-CalendarTime
bool ISO8601Parser::parse_calendar_time()
// https://tc39.es/proposal-temporal/#prod-AnnotatedTime
bool ISO8601Parser::parse_annotated_time()
{
// CalendarTime :
// TimeDesignator TimeSpec TimeZone[opt] Calendar[opt]
// TimeSpecWithOptionalTimeZoneNotAmbiguous Calendar[opt]
// AnnotatedTime :
// TimeDesignator TimeSpec TimeZone[opt] Annotations[opt]
// TimeSpecWithOptionalTimeZoneNotAmbiguous Annotations[opt]
{
StateTransaction transaction { *this };
if (parse_time_designator() && parse_time_spec()) {
(void)parse_time_zone();
(void)parse_calendar();
(void)parse_annotations();
transaction.commit();
return true;
}
@ -1013,34 +1135,34 @@ bool ISO8601Parser::parse_calendar_time()
StateTransaction transaction { *this };
if (!parse_time_spec_with_optional_time_zone_not_ambiguous())
return false;
(void)parse_calendar();
(void)parse_annotations();
transaction.commit();
return true;
}
// https://tc39.es/proposal-temporal/#prod-CalendarDateTime
bool ISO8601Parser::parse_calendar_date_time()
// https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime
bool ISO8601Parser::parse_annotated_date_time()
{
// CalendarDateTime :
// DateTime Calendar[opt]
// AnnotatedDateTime :
// DateTime Annotations[opt]
if (!parse_date_time())
return false;
(void)parse_calendar();
(void)parse_annotations();
return true;
}
// https://tc39.es/proposal-temporal/#prod-CalendarDateTimeTimeRequired
bool ISO8601Parser::parse_calendar_date_time_time_required()
// https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
bool ISO8601Parser::parse_annotated_date_time_time_required()
{
// CalendarDateTimeTimeRequired :
// Date TimeSpecSeparator TimeZone[opt] Calendar[opt]
// AnnotatedDateTimeTimeRequired :
// Date TimeSpecSeparator TimeZone[opt] Annotations[opt]
StateTransaction transaction { *this };
if (!parse_date())
return false;
if (!parse_time_spec_separator())
return false;
(void)parse_time_zone();
(void)parse_calendar();
(void)parse_annotations();
transaction.commit();
return true;
}
@ -1348,14 +1470,14 @@ bool ISO8601Parser::parse_duration()
bool ISO8601Parser::parse_temporal_instant_string()
{
// TemporalInstantString :
// Date TimeSpecSeparator[opt] TimeZoneOffsetRequired Calendar[opt]
// Date TimeSpecSeparator[opt] TimeZoneOffsetRequired Annotations[opt]
StateTransaction transaction { *this };
if (!parse_date())
return false;
(void)parse_time_spec_separator();
if (!parse_time_zone_offset_required())
return false;
(void)parse_calendar();
(void)parse_annotations();
transaction.commit();
return true;
}
@ -1364,8 +1486,8 @@ bool ISO8601Parser::parse_temporal_instant_string()
bool ISO8601Parser::parse_temporal_date_time_string()
{
// TemporalDateTimeString :
// CalendarDateTime
return parse_calendar_date_time();
// AnnotatedDateTime
return parse_annotated_date_time();
}
// https://tc39.es/proposal-temporal/#prod-TemporalDurationString
@ -1381,10 +1503,10 @@ bool ISO8601Parser::parse_temporal_month_day_string()
{
// TemporalMonthDayString :
// DateSpecMonthDay
// CalendarDateTime
// NOTE: Reverse order here because `DateSpecMonthDay` can be a subset of `CalendarDateTime`,
// AnnotatedDateTime
// NOTE: Reverse order here because `DateSpecMonthDay` can be a subset of `AnnotatedDateTime`,
// so we'd not attempt to parse that but may not exhaust the input string.
return parse_calendar_date_time()
return parse_annotated_date_time()
|| parse_date_spec_month_day();
}
@ -1392,12 +1514,12 @@ bool ISO8601Parser::parse_temporal_month_day_string()
bool ISO8601Parser::parse_temporal_time_string()
{
// TemporalTimeString :
// CalendarTime
// CalendarDateTimeTimeRequired
// NOTE: Reverse order here because `Time` can be a subset of `CalendarDateTimeTimeRequired`,
// AnnotatedTime
// AnnotatedDateTimeTimeRequired
// NOTE: Reverse order here because `AnnotatedTime` can be a subset of `AnnotatedDateTimeTimeRequired`,
// so we'd not attempt to parse that but may not exhaust the input string.
return parse_calendar_date_time_time_required()
|| parse_calendar_time();
return parse_annotated_date_time_time_required()
|| parse_annotated_time();
}
// https://tc39.es/proposal-temporal/#prod-TemporalYearMonthString
@ -1405,10 +1527,10 @@ bool ISO8601Parser::parse_temporal_year_month_string()
{
// TemporalYearMonthString :
// DateSpecYearMonth
// CalendarDateTime
// NOTE: Reverse order here because `DateSpecYearMonth` can be a subset of `CalendarDateTime`,
// AnnotatedDateTime
// NOTE: Reverse order here because `DateSpecYearMonth` can be a subset of `AnnotatedDateTime`,
// so we'd not attempt to parse that but may not exhaust the input string.
return parse_calendar_date_time()
return parse_annotated_date_time()
|| parse_date_spec_year_month();
}
@ -1416,14 +1538,14 @@ bool ISO8601Parser::parse_temporal_year_month_string()
bool ISO8601Parser::parse_temporal_zoned_date_time_string()
{
// TemporalZonedDateTimeString :
// Date TimeSpecSeparator[opt] TimeZoneNameRequired Calendar[opt]
// Date TimeSpecSeparator[opt] TimeZoneNameRequired Annotations[opt]
StateTransaction transaction { *this };
if (!parse_date())
return false;
(void)parse_time_spec_separator();
if (!parse_time_zone_name_required())
return false;
(void)parse_calendar();
(void)parse_annotations();
transaction.commit();
return true;
}
@ -1440,7 +1562,7 @@ bool ISO8601Parser::parse_temporal_zoned_date_time_string()
__JS_ENUMERATE(TemporalZonedDateTimeString, parse_temporal_zoned_date_time_string) \
__JS_ENUMERATE(TimeZoneIdentifier, parse_time_zone_identifier) \
__JS_ENUMERATE(TimeZoneNumericUTCOffset, parse_time_zone_numeric_utc_offset) \
__JS_ENUMERATE(CalendarName, parse_calendar_name) \
__JS_ENUMERATE(AnnotationValue, parse_annotation_value) \
__JS_ENUMERATE(DateMonth, parse_date_month)
Optional<ParseResult> parse_iso8601(Production production, StringView input)

View file

@ -13,6 +13,12 @@
namespace JS::Temporal {
struct Annotation {
bool critical { false };
StringView key;
StringView value;
};
struct ParseResult {
Optional<StringView> sign;
Optional<StringView> date_year;
@ -22,7 +28,6 @@ struct ParseResult {
Optional<StringView> time_minute;
Optional<StringView> time_second;
Optional<StringView> time_fraction;
Optional<StringView> calendar_name;
Optional<StringView> utc_designator;
Optional<StringView> time_zone_bracketed_annotation;
Optional<StringView> time_zone_numeric_utc_offset;
@ -42,6 +47,9 @@ struct ParseResult {
Optional<StringView> duration_minutes_fraction;
Optional<StringView> duration_whole_seconds;
Optional<StringView> duration_seconds_fraction;
Optional<StringView> annotation_key;
Optional<StringView> annotation_value;
Vector<Annotation> annotations;
};
enum class Production {
@ -54,7 +62,7 @@ enum class Production {
TemporalZonedDateTimeString,
TimeZoneIdentifier,
TimeZoneNumericUTCOffset,
CalendarName,
AnnotationValue,
DateMonth,
};
@ -97,6 +105,7 @@ public:
[[nodiscard]] bool parse_weeks_designator();
[[nodiscard]] bool parse_years_designator();
[[nodiscard]] bool parse_utc_designator();
[[nodiscard]] bool parse_annotation_critical_flag();
[[nodiscard]] bool parse_date_year();
[[nodiscard]] bool parse_date_month();
[[nodiscard]] bool parse_date_month_with_thirty_days();
@ -131,15 +140,23 @@ public:
[[nodiscard]] bool parse_time_zone_offset_required();
[[nodiscard]] bool parse_time_zone_name_required();
[[nodiscard]] bool parse_time_zone();
[[nodiscard]] bool parse_calendar_name();
[[nodiscard]] bool parse_calendar();
[[nodiscard]] bool parse_a_key_leading_char();
[[nodiscard]] bool parse_a_key_char();
[[nodiscard]] bool parse_a_val_char();
[[nodiscard]] bool parse_annotation_key_tail();
[[nodiscard]] bool parse_annotation_key();
[[nodiscard]] bool parse_annotation_value_component();
[[nodiscard]] bool parse_annotation_value_tail();
[[nodiscard]] bool parse_annotation_value();
[[nodiscard]] bool parse_annotation();
[[nodiscard]] bool parse_annotations();
[[nodiscard]] bool parse_time_spec();
[[nodiscard]] bool parse_time_spec_with_optional_time_zone_not_ambiguous();
[[nodiscard]] bool parse_time_spec_separator();
[[nodiscard]] bool parse_date_time();
[[nodiscard]] bool parse_calendar_time();
[[nodiscard]] bool parse_calendar_date_time();
[[nodiscard]] bool parse_calendar_date_time_time_required();
[[nodiscard]] bool parse_annotated_time();
[[nodiscard]] bool parse_annotated_date_time();
[[nodiscard]] bool parse_annotated_date_time_time_required();
[[nodiscard]] bool parse_duration_whole_seconds();
[[nodiscard]] bool parse_duration_seconds_fraction();
[[nodiscard]] bool parse_duration_seconds_part();

View file

@ -65,12 +65,12 @@ describe("errors", () => {
);
});
test("calendar annotation must match calendar grammar even though it's ignored", () => {
test("annotations must match annotation grammar even though they're ignored", () => {
expect(() => {
Temporal.Instant.from("1970-01-01T00:00Z[u-ca=SerenityOS]");
Temporal.Instant.from("1970-01-01T00:00Z[SerenityOS=cool]");
}).toThrowWithMessage(
RangeError,
"Invalid instant string '1970-01-01T00:00Z[u-ca=SerenityOS]'"
"Invalid instant string '1970-01-01T00:00Z[SerenityOS=cool]'"
);
});
});

View file

@ -31,11 +31,11 @@ describe("errors", () => {
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime");
});
test("from invalid calendar string", () => {
test("from invalid calendar identifier", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, {}, {});
expect(() => {
zonedDateTime.withCalendar("iso8602foobar");
}).toThrowWithMessage(RangeError, "Invalid calendar string 'iso8602foobar'");
}).toThrowWithMessage(RangeError, "Invalid calendar identifier 'iso8602foobar'");
});
});