/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "builtin/temporal/Temporal.h" #include "mozilla/Casting.h" #ifdef DEBUG # include "mozilla/CheckedInt.h" #endif #include "mozilla/FloatingPoint.h" #include "mozilla/Likely.h" #include "mozilla/MathAlgorithms.h" #include #include #include #include #include #include #include #include #include #include "jsnum.h" #include "jspubtd.h" #include "NamespaceImports.h" #include "builtin/temporal/Int128.h" #include "builtin/temporal/PlainDate.h" #include "builtin/temporal/PlainDateTime.h" #include "builtin/temporal/PlainMonthDay.h" #include "builtin/temporal/PlainTime.h" #include "builtin/temporal/PlainYearMonth.h" #include "builtin/temporal/TemporalRoundingMode.h" #include "builtin/temporal/TemporalUnit.h" #include "builtin/temporal/ZonedDateTime.h" #include "gc/Barrier.h" #include "js/Class.h" #include "js/Conversions.h" #include "js/ErrorReport.h" #include "js/friend/ErrorMessages.h" #include "js/Id.h" #include "js/Printer.h" #include "js/PropertyDescriptor.h" #include "js/PropertySpec.h" #include "js/RootingAPI.h" #include "js/String.h" #include "js/Value.h" #include "vm/BytecodeUtil.h" #include "vm/GlobalObject.h" #include "vm/JSAtomState.h" #include "vm/JSAtomUtils.h" #include "vm/JSContext.h" #include "vm/JSObject.h" #include "vm/ObjectOperations.h" #include "vm/Realm.h" #include "vm/StringType.h" #include "vm/JSObject-inl.h" #include "vm/ObjectOperations-inl.h" using namespace js; using namespace js::temporal; /** * GetOption ( options, property, type, values, default ) * * GetOption specialization when `type=string`. Default value handling must * happen in the caller, so we don't provide the `default` parameter here. */ static bool GetStringOption(JSContext* cx, Handle options, Handle property, MutableHandle string) { // Step 1. Rooted value(cx); if (!GetProperty(cx, options, options, property, &value)) { return false; } // Step 2. (Caller should fill in the fallback.) if (value.isUndefined()) { return true; } // Steps 3-4. (Not applicable when type=string) // Step 5. string.set(JS::ToString(cx, value)); if (!string) { return false; } // Step 6. (Not applicable in our implementation) // Step 7. return true; } /** * GetRoundingIncrementOption ( normalizedOptions, dividend, inclusive ) */ bool js::temporal::GetRoundingIncrementOption(JSContext* cx, Handle options, Increment* increment) { // Step 1. Rooted value(cx); if (!GetProperty(cx, options, options, cx->names().roundingIncrement, &value)) { return false; } // Step 2. if (value.isUndefined()) { *increment = Increment{1}; return true; } // Step 3. double number; if (!ToIntegerWithTruncation(cx, value, "roundingIncrement", &number)) { return false; } // Step 4. if (number < 1 || number > 1'000'000'000) { ToCStringBuf cbuf; const char* numStr = NumberToCString(&cbuf, number); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "roundingIncrement", numStr); return false; } // Step 5. *increment = Increment{uint32_t(number)}; return true; } /** * ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ) */ bool js::temporal::ValidateTemporalRoundingIncrement(JSContext* cx, Increment increment, int64_t dividend, bool inclusive) { MOZ_ASSERT(dividend > 0); MOZ_ASSERT_IF(!inclusive, dividend > 1); // Steps 1-2. int64_t maximum = inclusive ? dividend : dividend - 1; // Steps 3-4. if (increment.value() > maximum || dividend % increment.value() != 0) { Int32ToCStringBuf cbuf; const char* numStr = Int32ToCString(&cbuf, int32_t(increment.value())); // TODO: Better error message could be helpful. JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "roundingIncrement", numStr); return false; } // Step 5. return true; } static Handle ToPropertyName(JSContext* cx, TemporalUnitKey key) { switch (key) { case TemporalUnitKey::SmallestUnit: return cx->names().smallestUnit; case TemporalUnitKey::LargestUnit: return cx->names().largestUnit; case TemporalUnitKey::Unit: return cx->names().unit; } MOZ_CRASH("invalid temporal unit group"); } static const char* ToCString(TemporalUnitKey key) { switch (key) { case TemporalUnitKey::SmallestUnit: return "smallestUnit"; case TemporalUnitKey::LargestUnit: return "largestUnit"; case TemporalUnitKey::Unit: return "unit"; } MOZ_CRASH("invalid temporal unit group"); } static bool ToTemporalUnit(JSContext* cx, JSLinearString* str, TemporalUnitKey key, TemporalUnit* unit) { struct UnitMap { std::string_view name; TemporalUnit unit; }; static constexpr UnitMap mapping[] = { {"year", TemporalUnit::Year}, {"years", TemporalUnit::Year}, {"month", TemporalUnit::Month}, {"months", TemporalUnit::Month}, {"week", TemporalUnit::Week}, {"weeks", TemporalUnit::Week}, {"day", TemporalUnit::Day}, {"days", TemporalUnit::Day}, {"hour", TemporalUnit::Hour}, {"hours", TemporalUnit::Hour}, {"minute", TemporalUnit::Minute}, {"minutes", TemporalUnit::Minute}, {"second", TemporalUnit::Second}, {"seconds", TemporalUnit::Second}, {"millisecond", TemporalUnit::Millisecond}, {"milliseconds", TemporalUnit::Millisecond}, {"microsecond", TemporalUnit::Microsecond}, {"microseconds", TemporalUnit::Microsecond}, {"nanosecond", TemporalUnit::Nanosecond}, {"nanoseconds", TemporalUnit::Nanosecond}, }; // Compute the length of the longest name. constexpr size_t maxNameLength = std::max_element(std::begin(mapping), std::end(mapping), [](const auto& x, const auto& y) { return x.name.length() < y.name.length(); }) ->name.length(); // Twenty StringEqualsLiteral calls for each possible combination seems a bit // expensive, so let's instead copy the input name into a char array and rely // on the compiler to generate optimized code for the comparisons. size_t length = str->length(); if (length <= maxNameLength && StringIsAscii(str)) { char chars[maxNameLength] = {}; JS::LossyCopyLinearStringChars(chars, str, length); for (const auto& m : mapping) { if (m.name == std::string_view(chars, length)) { *unit = m.unit; return true; } } } if (auto chars = QuoteString(cx, str, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, ToCString(key), chars.get()); } return false; } static std::pair AllowedValues( TemporalUnitGroup unitGroup) { switch (unitGroup) { case TemporalUnitGroup::Date: return {TemporalUnit::Year, TemporalUnit::Day}; case TemporalUnitGroup::Time: return {TemporalUnit::Hour, TemporalUnit::Nanosecond}; case TemporalUnitGroup::DateTime: return {TemporalUnit::Year, TemporalUnit::Nanosecond}; case TemporalUnitGroup::DayTime: return {TemporalUnit::Day, TemporalUnit::Nanosecond}; } MOZ_CRASH("invalid temporal unit group"); } /** * GetTemporalUnitValuedOption ( normalizedOptions, key, unitGroup, default [ , * extraValues ] ) */ bool js::temporal::GetTemporalUnitValuedOption(JSContext* cx, Handle options, TemporalUnitKey key, TemporalUnitGroup unitGroup, TemporalUnit* unit) { // Steps 1-8. (Not applicable in our implementation.) // Step 9. Rooted value(cx); if (!GetStringOption(cx, options, ToPropertyName(cx, key), &value)) { return false; } // Caller should fill in the fallback. if (!value) { return true; } return GetTemporalUnitValuedOption(cx, value, key, unitGroup, unit); } /** * GetTemporalUnitValuedOption ( normalizedOptions, key, unitGroup, default [ , * extraValues ] ) */ bool js::temporal::GetTemporalUnitValuedOption(JSContext* cx, Handle value, TemporalUnitKey key, TemporalUnitGroup unitGroup, TemporalUnit* unit) { // Steps 1-9. (Not applicable in our implementation.) // Step 10. (Handled in caller.) Rooted linear(cx, value->ensureLinear(cx)); if (!linear) { return false; } // Caller should fill in the fallback. if (key == TemporalUnitKey::LargestUnit) { if (StringEqualsLiteral(linear, "auto")) { return true; } } // Step 11. if (!ToTemporalUnit(cx, linear, key, unit)) { return false; } auto allowedValues = AllowedValues(unitGroup); if (*unit < allowedValues.first || *unit > allowedValues.second) { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, ToCString(key), chars.get()); } return false; } return true; } /** * GetRoundingModeOption ( normalizedOptions, fallback ) */ bool js::temporal::GetRoundingModeOption(JSContext* cx, Handle options, TemporalRoundingMode* mode) { // Steps 1-2. Rooted string(cx); if (!GetStringOption(cx, options, cx->names().roundingMode, &string)) { return false; } // Caller should fill in the fallback. if (!string) { return true; } JSLinearString* linear = string->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "ceil")) { *mode = TemporalRoundingMode::Ceil; } else if (StringEqualsLiteral(linear, "floor")) { *mode = TemporalRoundingMode::Floor; } else if (StringEqualsLiteral(linear, "expand")) { *mode = TemporalRoundingMode::Expand; } else if (StringEqualsLiteral(linear, "trunc")) { *mode = TemporalRoundingMode::Trunc; } else if (StringEqualsLiteral(linear, "halfCeil")) { *mode = TemporalRoundingMode::HalfCeil; } else if (StringEqualsLiteral(linear, "halfFloor")) { *mode = TemporalRoundingMode::HalfFloor; } else if (StringEqualsLiteral(linear, "halfExpand")) { *mode = TemporalRoundingMode::HalfExpand; } else if (StringEqualsLiteral(linear, "halfTrunc")) { *mode = TemporalRoundingMode::HalfTrunc; } else if (StringEqualsLiteral(linear, "halfEven")) { *mode = TemporalRoundingMode::HalfEven; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "roundingMode", chars.get()); } return false; } return true; } #ifdef DEBUG template static bool IsValidMul(const T& x, const T& y) { return (mozilla::CheckedInt(x) * y).isValid(); } // Copied from mozilla::CheckedInt. template <> bool IsValidMul(const Int128& x, const Int128& y) { static constexpr auto min = Int128{1} << 127; static constexpr auto max = ~min; if (x == Int128{0} || y == Int128{0}) { return true; } if (x > Int128{0}) { return y > Int128{0} ? x <= max / y : y >= min / x; } return y > Int128{0} ? x >= min / y : y >= max / x; } #endif /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ Int128 js::temporal::RoundNumberToIncrement(const Int128& numerator, int64_t denominator, Increment increment, TemporalRoundingMode roundingMode) { MOZ_ASSERT(denominator > 0); MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); auto inc = Int128{increment.value()}; MOZ_ASSERT(IsValidMul(Int128{denominator}, inc), "unsupported overflow"); auto divisor = Int128{denominator} * inc; MOZ_ASSERT(divisor > Int128{0}); // Steps 1-8. auto rounded = Divide(numerator, divisor, roundingMode); // Step 9. MOZ_ASSERT(IsValidMul(rounded, inc), "unsupported overflow"); return rounded * inc; } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ int64_t js::temporal::RoundNumberToIncrement( int64_t x, int64_t increment, TemporalRoundingMode roundingMode) { MOZ_ASSERT(increment > 0); // Steps 1-8. int64_t rounded = Divide(x, increment, roundingMode); // Step 9. MOZ_ASSERT(IsValidMul(rounded, increment), "unsupported overflow"); return rounded * increment; } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ Int128 js::temporal::RoundNumberToIncrement(const Int128& x, const Int128& increment, TemporalRoundingMode roundingMode) { MOZ_ASSERT(increment > Int128{0}); // Steps 1-8. auto rounded = Divide(x, increment, roundingMode); // Step 9. MOZ_ASSERT(IsValidMul(rounded, increment), "unsupported overflow"); return rounded * increment; } template static inline constexpr bool IsSafeInteger(const IntT& x) { constexpr IntT MaxSafeInteger = IntT{int64_t(1) << 53}; constexpr IntT MinSafeInteger = -MaxSafeInteger; return MinSafeInteger < x && x < MaxSafeInteger; } /** * Return the real number value of the fraction |numerator / denominator|. * * As an optimization we multiply the remainder by 16 when computing the number * of digits after the decimal point, i.e. we compute four instead of one bit of * the fractional digits. The denominator is therefore required to not exceed * 2**(N - log2(16)), where N is the number of non-sign bits in the mantissa. */ template static double FractionToDoubleSlow(const T& numerator, const T& denominator) { MOZ_ASSERT(denominator > T{0}, "expected positive denominator"); MOZ_ASSERT(denominator <= (T{1} << (std::numeric_limits::digits - 4)), "denominator too large"); auto absValue = [](const T& value) { if constexpr (std::is_same_v) { return value.abs(); } else { // NB: Not std::abs, because std::abs(INT64_MIN) is undefined behavior. return mozilla::Abs(value); } }; using UnsignedT = decltype(absValue(T{0})); static_assert(!std::numeric_limits::is_signed); auto divrem = [](const UnsignedT& x, const UnsignedT& y) { if constexpr (std::is_same_v) { return x.divrem(y); } else { return std::pair{x / y, x % y}; } }; auto [quot, rem] = divrem(absValue(numerator), static_cast(denominator)); // Simple case when no remainder is present. if (rem == UnsignedT{0}) { double sign = numerator < T{0} ? -1 : 1; return sign * double(quot); } using Double = mozilla::FloatingPoint; // Significand including the implicit one of IEEE-754 floating point numbers. static constexpr uint32_t SignificandWidthWithImplicitOne = Double::kSignificandWidth + 1; // Number of leading zeros for a correctly adjusted significand. static constexpr uint32_t SignificandLeadingZeros = 64 - SignificandWidthWithImplicitOne; // Exponent bias for an integral significand. (`Double::kExponentBias` is the // bias for the binary fraction `1.xyz * 2**exp`. For an integral significand // the significand width has to be added to the bias.) static constexpr int32_t ExponentBias = Double::kExponentBias + Double::kSignificandWidth; // Significand, possibly unnormalized. uint64_t significand = 0; // Significand ignored msd bits. uint32_t ignoredBits = 0; // Read quotient, from most to least significant digit. Stop when the // significand got too large for double precision. int32_t shift = std::numeric_limits::digits; for (; shift != 0 && ignoredBits == 0; shift -= 4) { uint64_t digit = uint64_t(quot >> (shift - 4)) & 0xf; significand = significand * 16 + digit; ignoredBits = significand >> SignificandWidthWithImplicitOne; } // Read remainder, from most to least significant digit. Stop when the // remainder is zero or the significand got too large. int32_t fractionDigit = 0; for (; rem != UnsignedT{0} && ignoredBits == 0; fractionDigit++) { auto [digit, next] = divrem(rem * UnsignedT{16}, static_cast(denominator)); rem = next; significand = significand * 16 + uint64_t(digit); ignoredBits = significand >> SignificandWidthWithImplicitOne; } // Unbiased exponent. (`shift` remaining bits in the quotient, minus the // fractional digits.) int32_t exponent = shift - (fractionDigit * 4); // Significand got too large and some bits are now ignored. Adjust the // significand and exponent. if (ignoredBits != 0) { // significand // ___________|__________ // / | // [xxx················yyy| // \_/ \_/ // | | // ignoredBits extraBits // // `ignoredBits` have to be shifted back into the 53 bits of the significand // and `extraBits` has to be checked if the result has to be rounded up. // Number of ignored/extra bits in the significand. uint32_t extraBitsCount = 32 - mozilla::CountLeadingZeroes32(ignoredBits); MOZ_ASSERT(extraBitsCount > 0); // Extra bits in the significand. uint32_t extraBits = uint32_t(significand) & ((1 << extraBitsCount) - 1); // Move the ignored bits into the proper significand position and adjust the // exponent to reflect the now moved out extra bits. significand >>= extraBitsCount; exponent += extraBitsCount; MOZ_ASSERT((significand >> SignificandWidthWithImplicitOne) == 0, "no excess bits in the significand"); // When the most significant digit in the extra bits is set, we may need to // round the result. uint32_t msdExtraBit = extraBits >> (extraBitsCount - 1); if (msdExtraBit != 0) { // Extra bits, excluding the most significant digit. uint32_t extraBitExcludingMsdMask = (1 << (extraBitsCount - 1)) - 1; // Unprocessed bits in the quotient. auto bitsBelowExtraBits = quot & ((UnsignedT{1} << shift) - UnsignedT{1}); // Round up if the extra bit's msd is set and either the significand is // odd or any other bits below the extra bit's msd are non-zero. // // Bits below the extra bit's msd are: // 1. The remaining bits of the extra bits. // 2. Any bits below the extra bits. // 3. Any rest of the remainder. bool shouldRoundUp = (significand & 1) != 0 || (extraBits & extraBitExcludingMsdMask) != 0 || bitsBelowExtraBits != UnsignedT{0} || rem != UnsignedT{0}; if (shouldRoundUp) { // Add one to the significand bits. significand += 1; // If they overflow, the exponent must also be increased. if ((significand >> SignificandWidthWithImplicitOne) != 0) { exponent++; significand >>= 1; } } } } MOZ_ASSERT(significand > 0, "significand is non-zero"); MOZ_ASSERT((significand >> SignificandWidthWithImplicitOne) == 0, "no excess bits in the significand"); // Move the significand into the correct position and adjust the exponent // accordingly. uint32_t significandZeros = mozilla::CountLeadingZeroes64(significand); if (significandZeros < SignificandLeadingZeros) { uint32_t shift = SignificandLeadingZeros - significandZeros; significand >>= shift; exponent += shift; } else if (significandZeros > SignificandLeadingZeros) { uint32_t shift = significandZeros - SignificandLeadingZeros; significand <<= shift; exponent -= shift; } // Combine the individual bits of the double value and return it. uint64_t signBit = uint64_t(numerator < T{0} ? 1 : 0) << (Double::kExponentWidth + Double::kSignificandWidth); uint64_t exponentBits = static_cast(exponent + ExponentBias) << Double::kExponentShift; uint64_t significandBits = significand & Double::kSignificandBits; return mozilla::BitwiseCast(signBit | exponentBits | significandBits); } double js::temporal::FractionToDouble(int64_t numerator, int64_t denominator) { MOZ_ASSERT(denominator > 0); // Zero divided by any divisor is still zero. if (numerator == 0) { return 0; } // When both values can be represented as doubles, use double division to // compute the exact result. The result is exact, because double division is // guaranteed to return the exact result. if (MOZ_LIKELY(::IsSafeInteger(numerator) && ::IsSafeInteger(denominator))) { return double(numerator) / double(denominator); } // Otherwise call into |FractionToDoubleSlow| to compute the exact result. if (denominator <= (int64_t(1) << (std::numeric_limits::digits - 4))) { // Slightly faster, but still slow approach when |denominator| is small // enough to allow computing on int64 values. return FractionToDoubleSlow(numerator, denominator); } return FractionToDoubleSlow(Int128{numerator}, Int128{denominator}); } double js::temporal::FractionToDouble(const Int128& numerator, const Int128& denominator) { MOZ_ASSERT(denominator > Int128{0}); // Zero divided by any divisor is still zero. if (numerator == Int128{0}) { return 0; } // When both values can be represented as doubles, use double division to // compute the exact result. The result is exact, because double division is // guaranteed to return the exact result. if (MOZ_LIKELY(::IsSafeInteger(numerator) && ::IsSafeInteger(denominator))) { return double(numerator) / double(denominator); } // Otherwise call into |FractionToDoubleSlow| to compute the exact result. return FractionToDoubleSlow(numerator, denominator); } /** * GetTemporalShowCalendarNameOption ( normalizedOptions ) */ bool js::temporal::GetTemporalShowCalendarNameOption(JSContext* cx, Handle options, ShowCalendar* result) { // Step 1. Rooted calendarName(cx); if (!GetStringOption(cx, options, cx->names().calendarName, &calendarName)) { return false; } // Caller should fill in the fallback. if (!calendarName) { return true; } JSLinearString* linear = calendarName->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "auto")) { *result = ShowCalendar::Auto; } else if (StringEqualsLiteral(linear, "always")) { *result = ShowCalendar::Always; } else if (StringEqualsLiteral(linear, "never")) { *result = ShowCalendar::Never; } else if (StringEqualsLiteral(linear, "critical")) { *result = ShowCalendar::Critical; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "calendarName", chars.get()); } return false; } return true; } /** * GetTemporalFractionalSecondDigitsOption ( normalizedOptions ) */ bool js::temporal::GetTemporalFractionalSecondDigitsOption( JSContext* cx, Handle options, Precision* precision) { // Step 1. Rooted digitsValue(cx); if (!GetProperty(cx, options, options, cx->names().fractionalSecondDigits, &digitsValue)) { return false; } // Step 2. if (digitsValue.isUndefined()) { *precision = Precision::Auto(); return true; } // Step 3. if (!digitsValue.isNumber()) { // Step 3.a. JSString* string = JS::ToString(cx, digitsValue); if (!string) { return false; } JSLinearString* linear = string->ensureLinear(cx); if (!linear) { return false; } if (!StringEqualsLiteral(linear, "auto")) { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "fractionalSecondDigits", chars.get()); } return false; } // Step 3.b. *precision = Precision::Auto(); return true; } // Step 4. double digitCount = digitsValue.toNumber(); if (!std::isfinite(digitCount)) { ToCStringBuf cbuf; const char* numStr = NumberToCString(&cbuf, digitCount); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "fractionalSecondDigits", numStr); return false; } // Step 5. digitCount = std::floor(digitCount); // Step 6. if (digitCount < 0 || digitCount > 9) { ToCStringBuf cbuf; const char* numStr = NumberToCString(&cbuf, digitCount); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "fractionalSecondDigits", numStr); return false; } // Step 7. *precision = Precision{uint8_t(digitCount)}; return true; } /** * ToSecondsStringPrecisionRecord ( smallestUnit, fractionalDigitCount ) */ SecondsStringPrecision js::temporal::ToSecondsStringPrecision( TemporalUnit smallestUnit, Precision fractionalDigitCount) { MOZ_ASSERT(smallestUnit == TemporalUnit::Auto || smallestUnit >= TemporalUnit::Minute); MOZ_ASSERT(fractionalDigitCount == Precision::Auto() || fractionalDigitCount.value() <= 9); // Steps 1-5. switch (smallestUnit) { // Step 1. case TemporalUnit::Minute: return {Precision::Minute(), TemporalUnit::Minute, Increment{1}}; // Step 2. case TemporalUnit::Second: return {Precision{0}, TemporalUnit::Second, Increment{1}}; // Step 3. case TemporalUnit::Millisecond: return {Precision{3}, TemporalUnit::Millisecond, Increment{1}}; // Step 4. case TemporalUnit::Microsecond: return {Precision{6}, TemporalUnit::Microsecond, Increment{1}}; // Step 5. case TemporalUnit::Nanosecond: return {Precision{9}, TemporalUnit::Nanosecond, Increment{1}}; case TemporalUnit::Auto: break; case TemporalUnit::Year: case TemporalUnit::Month: case TemporalUnit::Week: case TemporalUnit::Day: case TemporalUnit::Hour: MOZ_CRASH("Unexpected temporal unit"); } // Step 6. (Not applicable in our implementation.) // Step 7. if (fractionalDigitCount == Precision::Auto()) { return {Precision::Auto(), TemporalUnit::Nanosecond, Increment{1}}; } static constexpr Increment increments[] = { Increment{1}, Increment{10}, Increment{100}, }; uint8_t digitCount = fractionalDigitCount.value(); // Step 8. if (digitCount == 0) { return {Precision{0}, TemporalUnit::Second, Increment{1}}; } // Step 9. if (digitCount <= 3) { return {fractionalDigitCount, TemporalUnit::Millisecond, increments[3 - digitCount]}; } // Step 10. if (digitCount <= 6) { return {fractionalDigitCount, TemporalUnit::Microsecond, increments[6 - digitCount]}; } // Step 11. MOZ_ASSERT(digitCount <= 9); // Step 12. return {fractionalDigitCount, TemporalUnit::Nanosecond, increments[9 - digitCount]}; } /** * GetTemporalOverflowOption ( normalizedOptions ) */ bool js::temporal::GetTemporalOverflowOption(JSContext* cx, Handle options, TemporalOverflow* result) { // Step 1. Rooted overflow(cx); if (!GetStringOption(cx, options, cx->names().overflow, &overflow)) { return false; } // Caller should fill in the fallback. if (!overflow) { return true; } JSLinearString* linear = overflow->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "constrain")) { *result = TemporalOverflow::Constrain; } else if (StringEqualsLiteral(linear, "reject")) { *result = TemporalOverflow::Reject; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "overflow", chars.get()); } return false; } return true; } /** * GetTemporalDisambiguationOption ( options ) */ bool js::temporal::GetTemporalDisambiguationOption( JSContext* cx, Handle options, TemporalDisambiguation* disambiguation) { // Step 1. (Not applicable) // Step 2. Rooted string(cx); if (!GetStringOption(cx, options, cx->names().disambiguation, &string)) { return false; } // Caller should fill in the fallback. if (!string) { return true; } JSLinearString* linear = string->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "compatible")) { *disambiguation = TemporalDisambiguation::Compatible; } else if (StringEqualsLiteral(linear, "earlier")) { *disambiguation = TemporalDisambiguation::Earlier; } else if (StringEqualsLiteral(linear, "later")) { *disambiguation = TemporalDisambiguation::Later; } else if (StringEqualsLiteral(linear, "reject")) { *disambiguation = TemporalDisambiguation::Reject; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "disambiguation", chars.get()); } return false; } return true; } /** * GetTemporalOffsetOption ( options, fallback ) */ bool js::temporal::GetTemporalOffsetOption(JSContext* cx, Handle options, TemporalOffset* offset) { // Step 1. (Not applicable in our implementation.) // Step 2. Rooted string(cx); if (!GetStringOption(cx, options, cx->names().offset, &string)) { return false; } // Caller should fill in the fallback. if (!string) { return true; } JSLinearString* linear = string->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "prefer")) { *offset = TemporalOffset::Prefer; } else if (StringEqualsLiteral(linear, "use")) { *offset = TemporalOffset::Use; } else if (StringEqualsLiteral(linear, "ignore")) { *offset = TemporalOffset::Ignore; } else if (StringEqualsLiteral(linear, "reject")) { *offset = TemporalOffset::Reject; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "offset", chars.get()); } return false; } return true; } /** * GetTemporalShowTimeZoneNameOption ( normalizedOptions ) */ bool js::temporal::GetTemporalShowTimeZoneNameOption(JSContext* cx, Handle options, ShowTimeZoneName* result) { // Step 1. Rooted timeZoneName(cx); if (!GetStringOption(cx, options, cx->names().timeZoneName, &timeZoneName)) { return false; } // Caller should fill in the fallback. if (!timeZoneName) { return true; } JSLinearString* linear = timeZoneName->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "auto")) { *result = ShowTimeZoneName::Auto; } else if (StringEqualsLiteral(linear, "never")) { *result = ShowTimeZoneName::Never; } else if (StringEqualsLiteral(linear, "critical")) { *result = ShowTimeZoneName::Critical; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "timeZoneName", chars.get()); } return false; } return true; } /** * GetTemporalShowOffsetOption ( normalizedOptions ) */ bool js::temporal::GetTemporalShowOffsetOption(JSContext* cx, Handle options, ShowOffset* result) { // Step 1. Rooted offset(cx); if (!GetStringOption(cx, options, cx->names().offset, &offset)) { return false; } // Caller should fill in the fallback. if (!offset) { return true; } JSLinearString* linear = offset->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "auto")) { *result = ShowOffset::Auto; } else if (StringEqualsLiteral(linear, "never")) { *result = ShowOffset::Never; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "offset", chars.get()); } return false; } return true; } /** * GetDirectionOption ( options ) */ bool js::temporal::GetDirectionOption(JSContext* cx, Handle direction, Direction* result) { JSLinearString* linear = direction->ensureLinear(cx); if (!linear) { return false; } if (StringEqualsLiteral(linear, "next")) { *result = Direction::Next; } else if (StringEqualsLiteral(linear, "previous")) { *result = Direction::Previous; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "direction", chars.get()); } return false; } return true; } /** * GetDirectionOption ( options ) */ bool js::temporal::GetDirectionOption(JSContext* cx, Handle options, Direction* result) { // Step 1. Rooted direction(cx); if (!GetStringOption(cx, options, cx->names().direction, &direction)) { return false; } if (!direction) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_MISSING_OPTION, "direction"); return false; } return GetDirectionOption(cx, direction, result); } template static JSObject* MaybeUnwrapIf(JSObject* object) { if (auto* unwrapped = object->maybeUnwrapIf()) { return unwrapped; } if constexpr (sizeof...(Ts) > 0) { return MaybeUnwrapIf(object); } return nullptr; } /** * IsPartialTemporalObject ( object ) */ bool js::temporal::ThrowIfTemporalLikeObject(JSContext* cx, Handle object) { // Step 1. (Handled in caller) // Step 2. if (auto* unwrapped = MaybeUnwrapIf(object)) { Rooted value(cx, ObjectValue(*object)); ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, value, nullptr, unwrapped->getClass()->name); return false; } Rooted property(cx); // Step 3. if (!GetProperty(cx, object, object, cx->names().calendar, &property)) { return false; } // Step 4. if (!property.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "calendar"); return false; } // Step 5. if (!GetProperty(cx, object, object, cx->names().timeZone, &property)) { return false; } // Step 6. if (!property.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "timeZone"); return false; } // Step 7. return true; } /** * ToPositiveIntegerWithTruncation ( argument ) */ bool js::temporal::ToPositiveIntegerWithTruncation(JSContext* cx, Handle value, const char* name, double* result) { // Step 1. double number; if (!ToIntegerWithTruncation(cx, value, name, &number)) { return false; } // Step 2. if (number <= 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_NUMBER, name); return false; } // Step 3. *result = number; return true; } /** * ToIntegerWithTruncation ( argument ) */ bool js::temporal::ToIntegerWithTruncation(JSContext* cx, Handle value, const char* name, double* result) { // Step 1. double number; if (!JS::ToNumber(cx, value, &number)) { return false; } // Step 2. if (!std::isfinite(number)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_INTEGER, name); return false; } // Step 3. *result = std::trunc(number) + (+0.0); // Add zero to convert -0 to +0. return true; } /** * GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, * fallbackSmallestUnit, smallestLargestDefaultUnit ) */ bool js::temporal::GetDifferenceSettings( JSContext* cx, TemporalDifference operation, Handle options, TemporalUnitGroup unitGroup, TemporalUnit smallestAllowedUnit, TemporalUnit fallbackSmallestUnit, TemporalUnit smallestLargestDefaultUnit, DifferenceSettings* result) { // Steps 1-2. auto largestUnit = TemporalUnit::Auto; if (!GetTemporalUnitValuedOption(cx, options, TemporalUnitKey::LargestUnit, unitGroup, &largestUnit)) { return false; } // Step 3. if (largestUnit > smallestAllowedUnit) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_UNIT_OPTION, TemporalUnitToString(largestUnit), "largestUnit"); return false; } // Step 4. auto roundingIncrement = Increment{1}; if (!GetRoundingIncrementOption(cx, options, &roundingIncrement)) { return false; } // Step 5. auto roundingMode = TemporalRoundingMode::Trunc; if (!GetRoundingModeOption(cx, options, &roundingMode)) { return false; } // Step 6. if (operation == TemporalDifference::Since) { roundingMode = NegateRoundingMode(roundingMode); } // Step 7. auto smallestUnit = fallbackSmallestUnit; if (!GetTemporalUnitValuedOption(cx, options, TemporalUnitKey::SmallestUnit, unitGroup, &smallestUnit)) { return false; } // Step 8. if (smallestUnit > smallestAllowedUnit) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_UNIT_OPTION, TemporalUnitToString(smallestUnit), "smallestUnit"); return false; } // Step 9. (Inlined call to LargerOfTwoTemporalUnits) auto defaultLargestUnit = std::min(smallestLargestDefaultUnit, smallestUnit); // Step 10. if (largestUnit == TemporalUnit::Auto) { largestUnit = defaultLargestUnit; } // Step 11. if (largestUnit > smallestUnit) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_UNIT_RANGE); return false; } // Steps 12-13. if (smallestUnit > TemporalUnit::Day) { // Step 12. auto maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit); // Step 13. if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, false)) { return false; } } // Step 14. *result = {smallestUnit, largestUnit, roundingMode, roundingIncrement}; return true; } static JSObject* CreateTemporalObject(JSContext* cx, JSProtoKey key) { Rooted proto(cx, &cx->global()->getObjectPrototype()); // The |Temporal| object is just a plain object with some "static" data // properties and some constructor properties. return NewTenuredObjectWithGivenProto(cx, proto); } /** * Initializes the Temporal Object and its standard built-in properties. */ static bool TemporalClassFinish(JSContext* cx, Handle temporal, Handle proto) { Rooted ctorId(cx); Rooted ctorValue(cx); auto defineProperty = [&](JSProtoKey protoKey, Handle name) { JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey); if (!ctor) { return false; } ctorId = NameToId(name); ctorValue.setObject(*ctor); return DefineDataProperty(cx, temporal, ctorId, ctorValue, 0); }; // Add the constructor properties. for (const auto& protoKey : { JSProto_Duration, JSProto_Instant, JSProto_PlainDate, JSProto_PlainDateTime, JSProto_PlainMonthDay, JSProto_PlainTime, JSProto_PlainYearMonth, JSProto_ZonedDateTime, }) { if (!defineProperty(protoKey, ClassName(protoKey, cx))) { return false; } } // ClassName(JSProto_TemporalNow) returns "TemporalNow", so we need to handle // it separately. if (!defineProperty(JSProto_TemporalNow, cx->names().Now)) { return false; } return true; } const JSClass TemporalObject::class_ = { "Temporal", JSCLASS_HAS_CACHED_PROTO(JSProto_Temporal), JS_NULL_CLASS_OPS, &TemporalObject::classSpec_, }; static const JSPropertySpec Temporal_properties[] = { JS_STRING_SYM_PS(toStringTag, "Temporal", JSPROP_READONLY), JS_PS_END, }; const ClassSpec TemporalObject::classSpec_ = { CreateTemporalObject, nullptr, nullptr, Temporal_properties, nullptr, nullptr, TemporalClassFinish, };