/* -*- 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/CheckedInt.h" #include "mozilla/Likely.h" #include "mozilla/Maybe.h" #include #include #include #include #include #include #include #include #include "jsfriendapi.h" #include "jsnum.h" #include "jspubtd.h" #include "NamespaceImports.h" #include "builtin/temporal/Instant.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/TemporalTypes.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/GCVector.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/BigIntType.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/PIC.h" #include "vm/PlainObject.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; } /** * GetOption ( options, property, type, values, default ) */ static bool GetNumberOption(JSContext* cx, Handle options, Handle property, double* number) { // 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 and 5. (Not applicable in our implementation) // Step 4.a. if (!JS::ToNumber(cx, value, number)) { return false; } // Step 4.b. (Caller must check for NaN values.) // Step 7. (Not applicable in our implementation) // Step 8. return true; } /** * ToTemporalRoundingIncrement ( normalizedOptions, dividend, inclusive ) */ bool js::temporal::ToTemporalRoundingIncrement(JSContext* cx, Handle options, Increment* increment) { // Step 1. double number = 1; if (!GetNumberOption(cx, options, cx->names().roundingIncrement, &number)) { return false; } // Step 3. (Reordered) number = std::trunc(number); // Steps 2 and 4. if (!std::isfinite(number) || 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; } *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, 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; } PropertyName* js::temporal::TemporalUnitToString(JSContext* cx, TemporalUnit unit) { switch (unit) { case TemporalUnit::Auto: break; case TemporalUnit::Year: return cx->names().year; case TemporalUnit::Month: return cx->names().month; case TemporalUnit::Week: return cx->names().week; case TemporalUnit::Day: return cx->names().day; case TemporalUnit::Hour: return cx->names().hour; case TemporalUnit::Minute: return cx->names().minute; case TemporalUnit::Second: return cx->names().second; case TemporalUnit::Millisecond: return cx->names().millisecond; case TemporalUnit::Microsecond: return cx->names().microsecond; case TemporalUnit::Nanosecond: return cx->names().nanosecond; } MOZ_CRASH("invalid temporal unit"); } 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"); } /** * GetTemporalUnit ( normalizedOptions, key, unitGroup, default [ , extraValues * ] ) */ bool js::temporal::GetTemporalUnit(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 GetTemporalUnit(cx, value, key, unitGroup, unit); } /** * GetTemporalUnit ( normalizedOptions, key, unitGroup, default [ , extraValues * ] ) */ bool js::temporal::GetTemporalUnit(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; } /** * ToTemporalRoundingMode ( normalizedOptions, fallback ) */ bool js::temporal::ToTemporalRoundingMode(JSContext* cx, Handle options, TemporalRoundingMode* mode) { // Step 1. 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; } static BigInt* Divide(JSContext* cx, Handle dividend, int64_t divisor, TemporalRoundingMode roundingMode) { MOZ_ASSERT(divisor > 0); Rooted div(cx, BigInt::createFromInt64(cx, divisor)); if (!div) { return nullptr; } Rooted quotient(cx); Rooted remainder(cx); if (!BigInt::divmod(cx, dividend, div, "ient, &remainder)) { return nullptr; } // No rounding needed when the remainder is zero. if (remainder->isZero()) { return quotient; } switch (roundingMode) { case TemporalRoundingMode::Ceil: { if (!remainder->isNegative()) { return BigInt::inc(cx, quotient); } return quotient; } case TemporalRoundingMode::Floor: { if (remainder->isNegative()) { return BigInt::dec(cx, quotient); } return quotient; } case TemporalRoundingMode::Trunc: // BigInt division truncates. return quotient; case TemporalRoundingMode::Expand: { if (!remainder->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } case TemporalRoundingMode::HalfCeil: { int64_t rem; MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); if (!remainder->isNegative()) { if (uint64_t(std::abs(rem)) * 2 >= uint64_t(divisor)) { return BigInt::inc(cx, quotient); } } else { if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { return BigInt::dec(cx, quotient); } } return quotient; } case TemporalRoundingMode::HalfFloor: { int64_t rem; MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); if (remainder->isNegative()) { if (uint64_t(std::abs(rem)) * 2 >= uint64_t(divisor)) { return BigInt::dec(cx, quotient); } } else { if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { return BigInt::inc(cx, quotient); } } return quotient; } case TemporalRoundingMode::HalfExpand: { int64_t rem; MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); if (uint64_t(std::abs(rem)) * 2 >= uint64_t(divisor)) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } return quotient; } case TemporalRoundingMode::HalfTrunc: { int64_t rem; MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } return quotient; } case TemporalRoundingMode::HalfEven: { int64_t rem; MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); if (uint64_t(std::abs(rem)) * 2 == uint64_t(divisor)) { bool isOdd = !quotient->isZero() && (quotient->digit(0) & 1) == 1; if (isOdd) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } } if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } return quotient; } } MOZ_CRASH("invalid rounding mode"); } static BigInt* Divide(JSContext* cx, Handle dividend, Handle divisor, TemporalRoundingMode roundingMode) { MOZ_ASSERT(!divisor->isNegative()); MOZ_ASSERT(!divisor->isZero()); Rooted quotient(cx); Rooted remainder(cx); if (!BigInt::divmod(cx, dividend, divisor, "ient, &remainder)) { return nullptr; } // No rounding needed when the remainder is zero. if (remainder->isZero()) { return quotient; } switch (roundingMode) { case TemporalRoundingMode::Ceil: { if (!remainder->isNegative()) { return BigInt::inc(cx, quotient); } return quotient; } case TemporalRoundingMode::Floor: { if (remainder->isNegative()) { return BigInt::dec(cx, quotient); } return quotient; } case TemporalRoundingMode::Trunc: // BigInt division truncates. return quotient; case TemporalRoundingMode::Expand: { if (!remainder->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } case TemporalRoundingMode::HalfCeil: { BigInt* rem = BigInt::add(cx, remainder, remainder); if (!rem) { return nullptr; } if (!remainder->isNegative()) { if (BigInt::absoluteCompare(rem, divisor) >= 0) { return BigInt::inc(cx, quotient); } } else { if (BigInt::absoluteCompare(rem, divisor) > 0) { return BigInt::dec(cx, quotient); } } return quotient; } case TemporalRoundingMode::HalfFloor: { BigInt* rem = BigInt::add(cx, remainder, remainder); if (!rem) { return nullptr; } if (remainder->isNegative()) { if (BigInt::absoluteCompare(rem, divisor) >= 0) { return BigInt::dec(cx, quotient); } } else { if (BigInt::absoluteCompare(rem, divisor) > 0) { return BigInt::inc(cx, quotient); } } return quotient; } case TemporalRoundingMode::HalfExpand: { BigInt* rem = BigInt::add(cx, remainder, remainder); if (!rem) { return nullptr; } if (BigInt::absoluteCompare(rem, divisor) >= 0) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } return quotient; } case TemporalRoundingMode::HalfTrunc: { BigInt* rem = BigInt::add(cx, remainder, remainder); if (!rem) { return nullptr; } if (BigInt::absoluteCompare(rem, divisor) > 0) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } return quotient; } case TemporalRoundingMode::HalfEven: { BigInt* rem = BigInt::add(cx, remainder, remainder); if (!rem) { return nullptr; } if (BigInt::absoluteCompare(rem, divisor) == 0) { bool isOdd = !quotient->isZero() && (quotient->digit(0) & 1) == 1; if (isOdd) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } } if (BigInt::absoluteCompare(rem, divisor) > 0) { if (!dividend->isNegative()) { return BigInt::inc(cx, quotient); } return BigInt::dec(cx, quotient); } return quotient; } } MOZ_CRASH("invalid rounding mode"); } static BigInt* RoundNumberToIncrementSlow(JSContext* cx, Handle x, int64_t divisor, int64_t increment, TemporalRoundingMode roundingMode) { // Steps 1-8. Rooted rounded(cx, Divide(cx, x, divisor, roundingMode)); if (!rounded) { return nullptr; } // We can skip the next step when |increment=1|. if (increment == 1) { return rounded; } // Step 9. Rooted inc(cx, BigInt::createFromInt64(cx, increment)); if (!inc) { return nullptr; } return BigInt::mul(cx, rounded, inc); } static BigInt* RoundNumberToIncrementSlow(JSContext* cx, Handle x, int64_t increment, TemporalRoundingMode roundingMode) { return RoundNumberToIncrementSlow(cx, x, increment, increment, roundingMode); } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ bool js::temporal::RoundNumberToIncrement(JSContext* cx, const Instant& x, int64_t increment, TemporalRoundingMode roundingMode, Instant* result) { MOZ_ASSERT(temporal::IsValidEpochInstant(x)); MOZ_ASSERT(increment > 0); MOZ_ASSERT(increment <= ToNanoseconds(TemporalUnit::Day)); // Fast path for the default case. if (increment == 1) { *result = x; return true; } // Dividing zero is always zero. if (x == Instant{}) { *result = x; return true; } // Fast-path when we can perform the whole computation with int64 values. if (auto num = x.toNanoseconds(); MOZ_LIKELY(num.isValid())) { // Steps 1-8. int64_t rounded = Divide(num.value(), increment, roundingMode); // Step 9. mozilla::CheckedInt64 checked = rounded; checked *= increment; if (MOZ_LIKELY(checked.isValid())) { *result = Instant::fromNanoseconds(checked.value()); return true; } } Rooted bi(cx, ToEpochNanoseconds(cx, x)); if (!bi) { return false; } auto* rounded = RoundNumberToIncrementSlow(cx, bi, increment, roundingMode); if (!rounded) { return false; } *result = ToInstant(rounded); return true; } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ bool js::temporal::RoundNumberToIncrement(JSContext* cx, int64_t numerator, TemporalUnit unit, Increment increment, TemporalRoundingMode roundingMode, double* result) { MOZ_ASSERT(unit >= TemporalUnit::Day); MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); // Take the slow path when the increment is too large. if (MOZ_UNLIKELY(increment > Increment{100'000})) { Rooted bi(cx, BigInt::createFromInt64(cx, numerator)); if (!bi) { return false; } Rooted denominator( cx, BigInt::createFromInt64(cx, ToNanoseconds(unit))); if (!denominator) { return false; } return RoundNumberToIncrement(cx, bi, denominator, increment, roundingMode, result); } int64_t divisor = ToNanoseconds(unit) * increment.value(); MOZ_ASSERT(divisor > 0); MOZ_ASSERT(divisor <= 8'640'000'000'000'000'000); // Division by one has no remainder. if (divisor == 1) { MOZ_ASSERT(increment == Increment{1}); *result = double(numerator); return true; } // Steps 1-8. int64_t rounded = Divide(numerator, divisor, roundingMode); // Step 9. mozilla::CheckedInt64 checked = rounded; checked *= increment.value(); if (checked.isValid()) { *result = double(checked.value()); return true; } Rooted bi(cx, BigInt::createFromInt64(cx, numerator)); if (!bi) { return false; } return RoundNumberToIncrement(cx, bi, unit, increment, roundingMode, result); } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ bool js::temporal::RoundNumberToIncrement( JSContext* cx, Handle numerator, TemporalUnit unit, Increment increment, TemporalRoundingMode roundingMode, double* result) { MOZ_ASSERT(unit >= TemporalUnit::Day); MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); // Take the slow path when the increment is too large. if (MOZ_UNLIKELY(increment > Increment{100'000})) { Rooted denominator( cx, BigInt::createFromInt64(cx, ToNanoseconds(unit))); if (!denominator) { return false; } return RoundNumberToIncrement(cx, numerator, denominator, increment, roundingMode, result); } int64_t divisor = ToNanoseconds(unit) * increment.value(); MOZ_ASSERT(divisor > 0); MOZ_ASSERT(divisor <= 8'640'000'000'000'000'000); // Division by one has no remainder. if (divisor == 1) { MOZ_ASSERT(increment == Increment{1}); *result = BigInt::numberValue(numerator); return true; } // Dividing zero is always zero. if (numerator->isZero()) { *result = 0; return true; } // All callers are already in the slow path, so we don't need to fast-path the // case when |x| can be represented by an int64 value. // Steps 1-9. auto* rounded = RoundNumberToIncrementSlow(cx, numerator, divisor, increment.value(), roundingMode); if (!rounded) { return false; } *result = BigInt::numberValue(rounded); return true; } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ bool js::temporal::RoundNumberToIncrement(JSContext* cx, int64_t numerator, int64_t denominator, Increment increment, TemporalRoundingMode roundingMode, double* result) { MOZ_ASSERT(denominator > 0); MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); // Dividing zero is always zero. if (numerator == 0) { *result = 0; return true; } // We don't have to adjust the divisor when |increment=1|. if (increment == Increment{1}) { int64_t divisor = denominator; int64_t rounded = Divide(numerator, divisor, roundingMode); *result = double(rounded); return true; } auto divisor = mozilla::CheckedInt64(denominator) * increment.value(); if (MOZ_LIKELY(divisor.isValid())) { MOZ_ASSERT(divisor.value() > 0); // Steps 1-8. int64_t rounded = Divide(numerator, divisor.value(), roundingMode); // Step 9. auto adjusted = mozilla::CheckedInt64(rounded) * increment.value(); if (MOZ_LIKELY(adjusted.isValid())) { *result = double(adjusted.value()); return true; } } // Slow path on overflow. Rooted bi(cx, BigInt::createFromInt64(cx, numerator)); if (!bi) { return false; } Rooted denom(cx, BigInt::createFromInt64(cx, denominator)); if (!denom) { return false; } return RoundNumberToIncrement(cx, bi, denom, increment, roundingMode, result); } /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ bool js::temporal::RoundNumberToIncrement( JSContext* cx, Handle numerator, Handle denominator, Increment increment, TemporalRoundingMode roundingMode, double* result) { MOZ_ASSERT(!denominator->isNegative()); MOZ_ASSERT(!denominator->isZero()); MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); // Dividing zero is always zero. if (numerator->isZero()) { *result = 0; return true; } // We don't have to adjust the divisor when |increment=1|. if (increment == Increment{1}) { auto divisor = denominator; auto* rounded = Divide(cx, numerator, divisor, roundingMode); if (!rounded) { return false; } *result = BigInt::numberValue(rounded); return true; } Rooted inc(cx, BigInt::createFromUint64(cx, increment.value())); if (!inc) { return false; } Rooted divisor(cx, BigInt::mul(cx, denominator, inc)); if (!divisor) { return false; } MOZ_ASSERT(!divisor->isNegative()); MOZ_ASSERT(!divisor->isZero()); // Steps 1-8. Rooted rounded(cx, Divide(cx, numerator, divisor, roundingMode)); if (!rounded) { return false; } // Step 9. auto* adjusted = BigInt::mul(cx, rounded, inc); if (!adjusted) { return false; } *result = BigInt::numberValue(adjusted); return true; } /** * ToCalendarNameOption ( normalizedOptions ) */ bool js::temporal::ToCalendarNameOption(JSContext* cx, Handle options, CalendarOption* 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 = CalendarOption::Auto; } else if (StringEqualsLiteral(linear, "always")) { *result = CalendarOption::Always; } else if (StringEqualsLiteral(linear, "never")) { *result = CalendarOption::Never; } else if (StringEqualsLiteral(linear, "critical")) { *result = CalendarOption::Critical; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "calendarName", chars.get()); } return false; } return true; } /** * ToFractionalSecondDigits ( normalizedOptions ) */ bool js::temporal::ToFractionalSecondDigits(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]}; } /** * ToTemporalOverflow ( normalizedOptions ) */ bool js::temporal::ToTemporalOverflow(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; } /** * ToTemporalDisambiguation ( options ) */ bool js::temporal::ToTemporalDisambiguation( 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; } /** * ToTemporalOffset ( options, fallback ) */ bool js::temporal::ToTemporalOffset(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; } /** * ToTimeZoneNameOption ( normalizedOptions ) */ bool js::temporal::ToTimeZoneNameOption(JSContext* cx, Handle options, TimeZoneNameOption* 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 = TimeZoneNameOption::Auto; } else if (StringEqualsLiteral(linear, "never")) { *result = TimeZoneNameOption::Never; } else if (StringEqualsLiteral(linear, "critical")) { *result = TimeZoneNameOption::Critical; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "timeZoneName", chars.get()); } return false; } return true; } /** * ToShowOffsetOption ( normalizedOptions ) */ bool js::temporal::ToShowOffsetOption(JSContext* cx, Handle options, ShowOffsetOption* result) { // FIXME: spec issue - should be renamed to ToOffsetOption to match the other // operations ToCalendarNameOption and ToTimeZoneNameOption. // // https://github.com/tc39/proposal-temporal/issues/2441 // 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 = ShowOffsetOption::Auto; } else if (StringEqualsLiteral(linear, "never")) { *result = ShowOffsetOption::Never; } else { if (auto chars = QuoteString(cx, linear, '"')) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "offset", chars.get()); } return false; } return true; } template static JSObject* MaybeUnwrapIf(JSObject* object) { if (auto* unwrapped = object->maybeUnwrapIf()) { return unwrapped; } if constexpr (sizeof...(Ts) > 0) { return MaybeUnwrapIf(object); } return nullptr; } // FIXME: spec issue - "Reject" is exclusively used for Promise rejection. The // existing `RejectPromise` abstract operation unconditionally rejects, whereas // this operation conditionally rejects. // https://github.com/tc39/proposal-temporal/issues/2534 /** * RejectTemporalLikeObject ( object ) */ bool js::temporal::RejectTemporalLikeObject(JSContext* cx, Handle object) { // Step 1. 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 2. if (!GetProperty(cx, object, object, cx->names().calendar, &property)) { return false; } // Step 3. if (!property.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "calendar"); return false; } // Step 4. if (!GetProperty(cx, object, object, cx->names().timeZone, &property)) { return false; } // Step 5. if (!property.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "timeZone"); return false; } // Step 6. 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; } /** * GetMethod ( V, P ) */ JSObject* js::temporal::GetMethod(JSContext* cx, Handle object, Handle name) { // Step 1. Rooted value(cx); if (!GetProperty(cx, object, object, name, &value)) { return nullptr; } // Steps 2-3. if (!IsCallable(value)) { if (auto chars = StringToNewUTF8CharsZ(cx, *name)) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_PROPERTY_NOT_CALLABLE, chars.get()); } return nullptr; } // Step 4. return &value.toObject(); } /** * CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] ) * * Implementation when |excludedKeys| and |excludedValues| are both empty lists. */ bool js::temporal::CopyDataProperties(JSContext* cx, Handle target, Handle source) { // Optimization for the common case when |source| is a native object. if (source->is()) { bool optimized = false; if (!CopyDataPropertiesNative(cx, target, source.as(), nullptr, &optimized)) { return false; } if (optimized) { return true; } } // Step 1-2. (Not applicable) // Step 3. JS::RootedVector keys(cx); if (!GetPropertyKeys( cx, source, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { return false; } // Step 4. Rooted> desc(cx); Rooted propValue(cx); for (size_t i = 0; i < keys.length(); i++) { Handle key = keys[i]; // Steps 4.a-b. (Not applicable) // Step 4.c.i. if (!GetOwnPropertyDescriptor(cx, source, key, &desc)) { return false; } // Step 4.c.ii. if (desc.isNothing() || !desc->enumerable()) { continue; } // Step 4.c.ii.1. if (!GetProperty(cx, source, source, key, &propValue)) { return false; } // Step 4.c.ii.2. (Not applicable) // Step 4.c.ii.3. if (!DefineDataProperty(cx, target, key, propValue)) { return false; } } // Step 5. return true; } /** * CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] ) * * Implementation when |excludedKeys| is an empty list and |excludedValues| is * the list «undefined». */ static bool CopyDataPropertiesIgnoreUndefined(JSContext* cx, Handle target, Handle source) { // Step 1-2. (Not applicable) // Step 3. JS::RootedVector keys(cx); if (!GetPropertyKeys( cx, source, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { return false; } // Step 4. Rooted> desc(cx); Rooted propValue(cx); for (size_t i = 0; i < keys.length(); i++) { Handle key = keys[i]; // Steps 4.a-b. (Not applicable) // Step 4.c.i. if (!GetOwnPropertyDescriptor(cx, source, key, &desc)) { return false; } // Step 4.c.ii. if (desc.isNothing() || !desc->enumerable()) { continue; } // Step 4.c.ii.1. if (!GetProperty(cx, source, source, key, &propValue)) { return false; } // Step 4.c.ii.2. if (propValue.isUndefined()) { continue; } // Step 4.c.ii.3. if (!DefineDataProperty(cx, target, key, propValue)) { return false; } } // Step 5. return true; } /** * SnapshotOwnProperties ( source, proto [, excludedKeys [, excludedValues ] ] ) */ PlainObject* js::temporal::SnapshotOwnProperties(JSContext* cx, Handle source) { // Step 1. Rooted copy(cx, NewPlainObjectWithProto(cx, nullptr)); if (!copy) { return nullptr; } // Steps 2-4. if (!CopyDataProperties(cx, copy, source)) { return nullptr; } // Step 3. return copy; } /** * SnapshotOwnProperties ( source, proto [, excludedKeys [, excludedValues ] ] ) * * Implementation when |excludedKeys| is an empty list and |excludedValues| is * the list «undefined». */ PlainObject* js::temporal::SnapshotOwnPropertiesIgnoreUndefined( JSContext* cx, Handle source) { // Step 1. Rooted copy(cx, NewPlainObjectWithProto(cx, nullptr)); if (!copy) { return nullptr; } // Steps 2-4. if (!CopyDataPropertiesIgnoreUndefined(cx, copy, source)) { return nullptr; } // Step 3. return copy; } /** * 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 (!GetTemporalUnit(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 (!ToTemporalRoundingIncrement(cx, options, &roundingIncrement)) { return false; } // Step 5. auto roundingMode = TemporalRoundingMode::Trunc; if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { return false; } // Step 6. if (operation == TemporalDifference::Since) { roundingMode = NegateTemporalRoundingMode(roundingMode); } // Step 7. auto smallestUnit = fallbackSmallestUnit; if (!GetTemporalUnit(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; } bool temporal::IsArrayIterationSane(JSContext* cx, bool* result) { auto* stubChain = ForOfPIC::getOrCreate(cx); if (!stubChain) { return false; } return stubChain->tryOptimizeArray(cx, result); } 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_Calendar, JSProto_Duration, JSProto_Instant, JSProto_PlainDate, JSProto_PlainDateTime, JSProto_PlainMonthDay, JSProto_PlainTime, JSProto_PlainYearMonth, JSProto_TimeZone, 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, };