diff options
Diffstat (limited to 'js/src/builtin/temporal/Temporal.cpp')
-rw-r--r-- | js/src/builtin/temporal/Temporal.cpp | 1850 |
1 files changed, 1850 insertions, 0 deletions
diff --git a/js/src/builtin/temporal/Temporal.cpp b/js/src/builtin/temporal/Temporal.cpp new file mode 100644 index 0000000000..3960a2832d --- /dev/null +++ b/js/src/builtin/temporal/Temporal.cpp @@ -0,0 +1,1850 @@ +/* -*- 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 <algorithm> +#include <cmath> +#include <cstdlib> +#include <initializer_list> +#include <iterator> +#include <stdint.h> +#include <string_view> +#include <utility> + +#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<JSObject*> options, + Handle<PropertyName*> property, + MutableHandle<JSString*> string) { + // Step 1. + Rooted<Value> 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<JSObject*> options, + Handle<PropertyName*> property, double* number) { + // Step 1. + Rooted<Value> 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<JSObject*> 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<PropertyName*> 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<TemporalUnit, TemporalUnit> 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<JSObject*> options, + TemporalUnitKey key, + TemporalUnitGroup unitGroup, + TemporalUnit* unit) { + // Steps 1-8. (Not applicable in our implementation.) + + // Step 9. + Rooted<JSString*> 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<JSString*> value, + TemporalUnitKey key, + TemporalUnitGroup unitGroup, + TemporalUnit* unit) { + // Steps 1-9. (Not applicable in our implementation.) + + // Step 10. (Handled in caller.) + + Rooted<JSLinearString*> 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<JSObject*> options, + TemporalRoundingMode* mode) { + // Step 1. + Rooted<JSString*> 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<BigInt*> dividend, int64_t divisor, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(divisor > 0); + + Rooted<BigInt*> div(cx, BigInt::createFromInt64(cx, divisor)); + if (!div) { + return nullptr; + } + + Rooted<BigInt*> quotient(cx); + Rooted<BigInt*> 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<BigInt*> dividend, + Handle<BigInt*> divisor, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(!divisor->isNegative()); + MOZ_ASSERT(!divisor->isZero()); + + Rooted<BigInt*> quotient(cx); + Rooted<BigInt*> 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<BigInt*> x, + int64_t divisor, int64_t increment, + TemporalRoundingMode roundingMode) { + // Steps 1-8. + Rooted<BigInt*> 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<BigInt*> inc(cx, BigInt::createFromInt64(cx, increment)); + if (!inc) { + return nullptr; + } + return BigInt::mul(cx, rounded, inc); +} + +static BigInt* RoundNumberToIncrementSlow(JSContext* cx, Handle<BigInt*> 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<BigInt*> 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<BigInt*> bi(cx, BigInt::createFromInt64(cx, numerator)); + if (!bi) { + return false; + } + + Rooted<BigInt*> 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<BigInt*> 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<BigInt*> 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<BigInt*> 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<BigInt*> bi(cx, BigInt::createFromInt64(cx, numerator)); + if (!bi) { + return false; + } + + Rooted<BigInt*> 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<BigInt*> numerator, Handle<BigInt*> 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<BigInt*> inc(cx, BigInt::createFromUint64(cx, increment.value())); + if (!inc) { + return false; + } + + Rooted<BigInt*> divisor(cx, BigInt::mul(cx, denominator, inc)); + if (!divisor) { + return false; + } + MOZ_ASSERT(!divisor->isNegative()); + MOZ_ASSERT(!divisor->isZero()); + + // Steps 1-8. + Rooted<BigInt*> 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<JSObject*> options, + CalendarOption* result) { + // Step 1. + Rooted<JSString*> 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<JSObject*> options, + Precision* precision) { + // Step 1. + Rooted<Value> 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<JSObject*> options, + TemporalOverflow* result) { + // Step 1. + Rooted<JSString*> 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<JSObject*> options, + TemporalDisambiguation* disambiguation) { + // Step 1. (Not applicable) + + // Step 2. + Rooted<JSString*> 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<JSObject*> options, + TemporalOffset* offset) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<JSString*> 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<JSObject*> options, + TimeZoneNameOption* result) { + // Step 1. + Rooted<JSString*> 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<JSObject*> 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<JSString*> 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 <typename T, typename... Ts> +static JSObject* MaybeUnwrapIf(JSObject* object) { + if (auto* unwrapped = object->maybeUnwrapIf<T>()) { + return unwrapped; + } + if constexpr (sizeof...(Ts) > 0) { + return MaybeUnwrapIf<Ts...>(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<JSObject*> object) { + // Step 1. + if (auto* unwrapped = + MaybeUnwrapIf<PlainDateObject, PlainDateTimeObject, + PlainMonthDayObject, PlainTimeObject, + PlainYearMonthObject, ZonedDateTimeObject>(object)) { + Rooted<Value> value(cx, ObjectValue(*object)); + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, value, + nullptr, unwrapped->getClass()->name); + return false; + } + + Rooted<Value> 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> 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> 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<JSObject*> object, + Handle<PropertyName*> name) { + // Step 1. + Rooted<Value> 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<PlainObject*> target, + Handle<JSObject*> source) { + // Optimization for the common case when |source| is a native object. + if (source->is<NativeObject>()) { + bool optimized = false; + if (!CopyDataPropertiesNative(cx, target, source.as<NativeObject>(), + nullptr, &optimized)) { + return false; + } + if (optimized) { + return true; + } + } + + // Step 1-2. (Not applicable) + + // Step 3. + JS::RootedVector<PropertyKey> keys(cx); + if (!GetPropertyKeys( + cx, source, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { + return false; + } + + // Step 4. + Rooted<mozilla::Maybe<PropertyDescriptor>> desc(cx); + Rooted<Value> propValue(cx); + for (size_t i = 0; i < keys.length(); i++) { + Handle<PropertyKey> 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<PlainObject*> target, + Handle<JSObject*> source) { + // Step 1-2. (Not applicable) + + // Step 3. + JS::RootedVector<PropertyKey> keys(cx); + if (!GetPropertyKeys( + cx, source, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { + return false; + } + + // Step 4. + Rooted<mozilla::Maybe<PropertyDescriptor>> desc(cx); + Rooted<Value> propValue(cx); + for (size_t i = 0; i < keys.length(); i++) { + Handle<PropertyKey> 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<JSObject*> source) { + // Step 1. + Rooted<PlainObject*> 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<JSObject*> source) { + // Step 1. + Rooted<PlainObject*> 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<PlainObject*> 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<JSObject*> proto(cx, &cx->global()->getObjectPrototype()); + + // The |Temporal| object is just a plain object with some "static" data + // properties and some constructor properties. + return NewTenuredObjectWithGivenProto<TemporalObject>(cx, proto); +} + +/** + * Initializes the Temporal Object and its standard built-in properties. + */ +static bool TemporalClassFinish(JSContext* cx, Handle<JSObject*> temporal, + Handle<JSObject*> proto) { + Rooted<PropertyKey> ctorId(cx); + Rooted<Value> ctorValue(cx); + auto defineProperty = [&](JSProtoKey protoKey, Handle<PropertyName*> 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, +}; |