From 8dd16259287f58f9273002717ec4d27e97127719 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:43:14 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- js/src/builtin/Array.cpp | 297 +- js/src/builtin/Array.h | 140 +- js/src/builtin/AtomicsObject.cpp | 5 +- js/src/builtin/Boolean-inl.h | 3 +- js/src/builtin/DataViewObject.cpp | 80 +- js/src/builtin/DataViewObject.h | 6 + js/src/builtin/Eval.cpp | 46 +- js/src/builtin/MapObject.cpp | 2 - js/src/builtin/ModuleObject.cpp | 338 +- js/src/builtin/ModuleObject.h | 19 +- js/src/builtin/ReflectParse.cpp | 18 +- js/src/builtin/SelfHostingDefines.h | 1 + js/src/builtin/Set.js | 2 - js/src/builtin/ShadowRealm.cpp | 4 +- js/src/builtin/Sorting-inl.h | 307 + js/src/builtin/Sorting.h | 179 + js/src/builtin/Sorting.js | 120 - js/src/builtin/TestingFunctions.cpp | 40 +- js/src/builtin/TypedArray.js | 119 +- js/src/builtin/temporal/Calendar.cpp | 559 +- js/src/builtin/temporal/Calendar.h | 35 +- js/src/builtin/temporal/Duration.cpp | 7336 ++++++++++-------------- js/src/builtin/temporal/Duration.h | 225 +- js/src/builtin/temporal/Instant.cpp | 403 +- js/src/builtin/temporal/Instant.h | 42 +- js/src/builtin/temporal/Int128.cpp | 161 + js/src/builtin/temporal/Int128.h | 742 +++ js/src/builtin/temporal/Int96.cpp | 6 +- js/src/builtin/temporal/Int96.h | 2 +- js/src/builtin/temporal/PlainDate.cpp | 476 +- js/src/builtin/temporal/PlainDate.h | 63 +- js/src/builtin/temporal/PlainDateTime.cpp | 358 +- js/src/builtin/temporal/PlainDateTime.h | 26 +- js/src/builtin/temporal/PlainMonthDay.cpp | 48 +- js/src/builtin/temporal/PlainTime.cpp | 911 +-- js/src/builtin/temporal/PlainTime.h | 31 +- js/src/builtin/temporal/PlainYearMonth.cpp | 216 +- js/src/builtin/temporal/Temporal.cpp | 811 +-- js/src/builtin/temporal/Temporal.h | 42 +- js/src/builtin/temporal/TemporalFields.cpp | 8 +- js/src/builtin/temporal/TemporalFields.h | 3 +- js/src/builtin/temporal/TemporalNow.cpp | 4 +- js/src/builtin/temporal/TemporalParser.cpp | 284 +- js/src/builtin/temporal/TemporalParser.h | 7 +- js/src/builtin/temporal/TemporalRoundingMode.h | 282 + js/src/builtin/temporal/TemporalTypes.h | 240 +- js/src/builtin/temporal/TemporalUnit.h | 48 + js/src/builtin/temporal/TimeZone.cpp | 171 +- js/src/builtin/temporal/TimeZone.h | 5 +- js/src/builtin/temporal/ToString.cpp | 13 +- js/src/builtin/temporal/ZonedDateTime.cpp | 1003 ++-- js/src/builtin/temporal/ZonedDateTime.h | 108 +- js/src/builtin/temporal/moz.build | 1 + 53 files changed, 7955 insertions(+), 8441 deletions(-) create mode 100644 js/src/builtin/Sorting-inl.h create mode 100644 js/src/builtin/Sorting.h delete mode 100644 js/src/builtin/Sorting.js create mode 100644 js/src/builtin/temporal/Int128.cpp create mode 100644 js/src/builtin/temporal/Int128.h (limited to 'js/src/builtin') diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp index 2ced07b6b9..868fae8bcf 100644 --- a/js/src/builtin/Array.cpp +++ b/js/src/builtin/Array.cpp @@ -51,6 +51,7 @@ # include "vm/TupleType.h" #endif +#include "builtin/Sorting-inl.h" #include "vm/ArgumentsObject-inl.h" #include "vm/ArrayObject-inl.h" #include "vm/GeckoProfiler-inl.h" @@ -439,7 +440,7 @@ bool js::GetElements(JSContext* cx, HandleObject aobj, uint32_t length, if (aobj->is()) { Handle typedArray = aobj.as(); if (typedArray->length().valueOr(0) == length) { - return TypedArrayObject::getElements(cx, typedArray, vp); + return TypedArrayObject::getElements(cx, typedArray, length, vp); } } @@ -2180,44 +2181,16 @@ static bool ArraySortWithoutComparator(JSContext* cx, Handle obj, return true; } -void ArraySortData::init(JSObject* obj, JSObject* comparator, ValueVector&& vec, - uint32_t length, uint32_t denseLen) { - MOZ_ASSERT(!vec.empty(), "must have items to sort"); - MOZ_ASSERT(denseLen <= length); - - obj_ = obj; - comparator_ = comparator; - - this->length = length; - this->denseLen = denseLen; - this->vec = std::move(vec); - - auto getComparatorKind = [](JSContext* cx, JSObject* comparator) { - if (!comparator->is()) { - return ComparatorKind::Unoptimized; - } - JSFunction* fun = &comparator->as(); - if (!fun->hasJitEntry() || fun->isClassConstructor()) { - return ComparatorKind::Unoptimized; - } - if (fun->realm() == cx->realm() && fun->nargs() <= ComparatorActualArgs) { - return ComparatorKind::JSSameRealmNoRectifier; - } - return ComparatorKind::JS; - }; - comparatorKind_ = getComparatorKind(cx(), comparator); -} - // This function handles sorting without a comparator function (or with a // trivial comparator function that we can pattern match) by calling // ArraySortWithoutComparator. // // If there's a non-trivial comparator function, it initializes the -// ArraySortData struct for ArraySortData::sortWithComparator. This function -// must be called next to perform the sorting. +// ArraySortData struct for ArraySortData::sortArrayWithComparator. This +// function must be called next to perform the sorting. // -// This is separate from ArraySortData::sortWithComparator because it lets the -// compiler generate better code for ArraySortData::sortWithComparator. +// This is separate from ArraySortData::sortArrayWithComparator because it lets +// the compiler generate better code for ArraySortData::sortArrayWithComparator. // // https://tc39.es/ecma262/#sec-array.prototype.sort // 23.1.3.30 Array.prototype.sort ( comparefn ) @@ -2280,7 +2253,7 @@ static MOZ_ALWAYS_INLINE bool ArraySortPrologue(JSContext* cx, uint32_t len = uint32_t(length); // Merge sort requires extra scratch space. - bool needsScratchSpace = len >= ArraySortData::InsertionSortLimit; + bool needsScratchSpace = len > ArraySortData::InsertionSortMaxLength; Rooted vec(cx); if (MOZ_UNLIKELY(!vec.reserve(needsScratchSpace ? (2 * len) : len))) { @@ -2323,13 +2296,13 @@ static MOZ_ALWAYS_INLINE bool ArraySortPrologue(JSContext* cx, } d->init(obj, &comparefn.toObject(), std::move(vec.get()), len, denseLen); - // Continue in ArraySortData::sortWithComparator. + // Continue in ArraySortData::sortArrayWithComparator. MOZ_ASSERT(!*done); return true; } -static ArraySortResult CallComparatorSlow(ArraySortData* d, const Value& x, - const Value& y) { +ArraySortResult js::CallComparatorSlow(ArraySortData* d, const Value& x, + const Value& y) { JSContext* cx = d->cx(); FixedInvokeArgs<2> callArgs(cx); callArgs[0].set(x); @@ -2343,230 +2316,15 @@ static ArraySortResult CallComparatorSlow(ArraySortData* d, const Value& x, return ArraySortResult::Done; } -static MOZ_ALWAYS_INLINE ArraySortResult -MaybeYieldToComparator(ArraySortData* d, const Value& x, const Value& y) { - // https://tc39.es/ecma262/#sec-comparearrayelements - // 23.1.3.30.2 CompareArrayElements ( x, y, comparefn ) - - // Steps 1-2. - if (x.isUndefined()) { - d->setComparatorReturnValue(Int32Value(y.isUndefined() ? 0 : 1)); - return ArraySortResult::Done; - } - - // Step 3. - if (y.isUndefined()) { - d->setComparatorReturnValue(Int32Value(-1)); - return ArraySortResult::Done; - } - - // Step 4. Yield to the JIT trampoline (or js::array_sort) if the comparator - // is a JS function we can call more efficiently from JIT code. - auto kind = d->comparatorKind(); - if (MOZ_LIKELY(kind != ArraySortData::ComparatorKind::Unoptimized)) { - d->setComparatorArgs(x, y); - return (kind == ArraySortData::ComparatorKind::JSSameRealmNoRectifier) - ? ArraySortResult::CallJSSameRealmNoRectifier - : ArraySortResult::CallJS; - } - return CallComparatorSlow(d, x, y); -} - -static MOZ_ALWAYS_INLINE bool RvalIsLessOrEqual(ArraySortData* data, - bool* lessOrEqual) { - // https://tc39.es/ecma262/#sec-comparearrayelements - // 23.1.3.30.2 CompareArrayElements ( x, y, comparefn ) - - // Fast path for int32 return values. - Value rval = data->comparatorReturnValue(); - if (MOZ_LIKELY(rval.isInt32())) { - *lessOrEqual = rval.toInt32() <= 0; - return true; - } - - // Step 4.a. - Rooted rvalRoot(data->cx(), rval); - double d; - if (MOZ_UNLIKELY(!ToNumber(data->cx(), rvalRoot, &d))) { - return false; - } - - // Step 4.b-c. - *lessOrEqual = std::isnan(d) ? true : (d <= 0); - return true; -} - -static void CopyValues(Value* out, const Value* list, uint32_t start, - uint32_t end) { - for (uint32_t i = start; i <= end; i++) { - out[i] = list[i]; - } -} - // static -ArraySortResult ArraySortData::sortWithComparator(ArraySortData* d) { - JSContext* cx = d->cx(); - auto& vec = d->vec; - - // This function is like a generator that is called repeatedly from the JIT - // trampoline or js::array_sort. Resume the sorting algorithm where we left - // off before calling the comparator. - switch (d->state) { - case State::Initial: - break; - case State::InsertionSortCall1: - goto insertion_sort_call1; - case State::InsertionSortCall2: - goto insertion_sort_call2; - case State::MergeSortCall1: - goto merge_sort_call1; - case State::MergeSortCall2: - goto merge_sort_call2; - } - - d->list = vec.begin(); - - // Use insertion sort for small arrays. - if (d->denseLen < InsertionSortLimit) { - for (d->i = 1; d->i < d->denseLen; d->i++) { - d->item = vec[d->i]; - d->j = d->i - 1; - do { - { - ArraySortResult res = MaybeYieldToComparator(d, vec[d->j], d->item); - if (res != ArraySortResult::Done) { - d->state = State::InsertionSortCall1; - return res; - } - } - insertion_sort_call1: - bool lessOrEqual; - if (!RvalIsLessOrEqual(d, &lessOrEqual)) { - return ArraySortResult::Failure; - } - if (lessOrEqual) { - break; - } - vec[d->j + 1] = vec[d->j]; - } while (d->j-- > 0); - vec[d->j + 1] = d->item; - } - } else { - static constexpr size_t InitialWindowSize = 4; - - // Use insertion sort for initial ranges. - for (d->start = 0; d->start < d->denseLen - 1; - d->start += InitialWindowSize) { - d->end = - std::min(d->start + InitialWindowSize - 1, d->denseLen - 1); - for (d->i = d->start + 1; d->i <= d->end; d->i++) { - d->item = vec[d->i]; - d->j = d->i - 1; - do { - { - ArraySortResult res = MaybeYieldToComparator(d, vec[d->j], d->item); - if (res != ArraySortResult::Done) { - d->state = State::InsertionSortCall2; - return res; - } - } - insertion_sort_call2: - bool lessOrEqual; - if (!RvalIsLessOrEqual(d, &lessOrEqual)) { - return ArraySortResult::Failure; - } - if (lessOrEqual) { - break; - } - vec[d->j + 1] = vec[d->j]; - } while (d->j-- > d->start); - vec[d->j + 1] = d->item; - } - } - - // Merge sort. Set d->out to scratch space initially. - d->out = vec.begin() + d->denseLen; - for (d->windowSize = InitialWindowSize; d->windowSize < d->denseLen; - d->windowSize *= 2) { - for (d->start = 0; d->start < d->denseLen; - d->start += 2 * d->windowSize) { - // The midpoint between the two subarrays. - d->mid = d->start + d->windowSize - 1; - - // To keep from going over the edge. - d->end = std::min(d->start + 2 * d->windowSize - 1, - d->denseLen - 1); - - // Merge comparator-sorted slices list[start..<=mid] and - // list[mid+1..<=end], storing the merged sequence in out[start..<=end]. - - // Skip lopsided runs to avoid doing useless work. - if (d->mid >= d->end) { - CopyValues(d->out, d->list, d->start, d->end); - continue; - } - - // Skip calling the comparator if the sub-list is already sorted. - { - ArraySortResult res = - MaybeYieldToComparator(d, d->list[d->mid], d->list[d->mid + 1]); - if (res != ArraySortResult::Done) { - d->state = State::MergeSortCall1; - return res; - } - } - merge_sort_call1: - bool lessOrEqual; - if (!RvalIsLessOrEqual(d, &lessOrEqual)) { - return ArraySortResult::Failure; - } - if (lessOrEqual) { - CopyValues(d->out, d->list, d->start, d->end); - continue; - } - - d->i = d->start; - d->j = d->mid + 1; - d->k = d->start; - - while (d->i <= d->mid && d->j <= d->end) { - { - ArraySortResult res = - MaybeYieldToComparator(d, d->list[d->i], d->list[d->j]); - if (res != ArraySortResult::Done) { - d->state = State::MergeSortCall2; - return res; - } - } - merge_sort_call2: - bool lessOrEqual; - if (!RvalIsLessOrEqual(d, &lessOrEqual)) { - return ArraySortResult::Failure; - } - d->out[d->k++] = lessOrEqual ? d->list[d->i++] : d->list[d->j++]; - } - - // Empty out any remaining elements. Use local variables to let the - // compiler generate more efficient code. - Value* out = d->out; - Value* list = d->list; - uint32_t k = d->k; - uint32_t mid = d->mid; - uint32_t end = d->end; - for (uint32_t i = d->i; i <= mid; i++) { - out[k++] = list[i]; - } - for (uint32_t j = d->j; j <= end; j++) { - out[k++] = list[j]; - } - } - - // Swap both lists. - std::swap(d->list, d->out); - } +ArraySortResult ArraySortData::sortArrayWithComparator(ArraySortData* d) { + ArraySortResult result = sortWithComparatorShared(d); + if (result != ArraySortResult::Done) { + return result; } // Copy elements to the array. + JSContext* cx = d->cx(); Rooted obj(cx, d->obj_); if (!SetArrayElements(cx, obj, 0, d->denseLen, d->list)) { return ArraySortResult::Failure; @@ -2600,9 +2358,9 @@ bool js::array_sort(JSContext* cx, unsigned argc, Value* vp) { Rooted data(cx, cx); - // On all return paths other than ArraySortWithComparatorLoop returning Done, - // we call freeMallocData to not fail debug assertions. This matches the JIT - // trampoline where we can't rely on C++ destructors. + // On all return paths other than ArraySortData::sortArrayWithComparator + // returning Done, we call freeMallocData to not fail debug assertions. This + // matches the JIT trampoline where we can't rely on C++ destructors. auto freeData = mozilla::MakeScopeExit([&]() { data.get().freeMallocData(); }); @@ -2620,7 +2378,8 @@ bool js::array_sort(JSContext* cx, unsigned argc, Value* vp) { Rooted rval(cx); while (true) { - ArraySortResult res = ArraySortData::sortWithComparator(data.address()); + ArraySortResult res = + ArraySortData::sortArrayWithComparator(data.address()); switch (res) { case ArraySortResult::Failure: return false; @@ -2666,21 +2425,7 @@ ArraySortResult js::ArraySortFromJit(JSContext* cx, return ArraySortResult::Done; } - return ArraySortData::sortWithComparator(data); -} - -ArraySortData::ArraySortData(JSContext* cx) : cx_(cx) { -#ifdef DEBUG - cx_->liveArraySortDataInstances++; -#endif -} - -void ArraySortData::freeMallocData() { - vec.clearAndFree(); -#ifdef DEBUG - MOZ_ASSERT(cx_->liveArraySortDataInstances > 0); - cx_->liveArraySortDataInstances--; -#endif + return ArraySortData::sortArrayWithComparator(data); } void ArraySortData::trace(JSTracer* trc) { diff --git a/js/src/builtin/Array.h b/js/src/builtin/Array.h index f861505e7b..1a8335f0d4 100644 --- a/js/src/builtin/Array.h +++ b/js/src/builtin/Array.h @@ -15,6 +15,8 @@ namespace js { +enum class ArraySortResult : uint32_t; + namespace jit { class TrampolineNativeFrameLayout; } @@ -172,144 +174,6 @@ extern bool ArrayLengthGetter(JSContext* cx, HandleObject obj, HandleId id, extern bool ArrayLengthSetter(JSContext* cx, HandleObject obj, HandleId id, HandleValue v, ObjectOpResult& result); -// Note: we use uint32_t because the JIT code uses branch32. -enum class ArraySortResult : uint32_t { - Failure, - Done, - CallJS, - CallJSSameRealmNoRectifier -}; - -// We use a JIT trampoline to optimize sorting with a comparator function. The -// trampoline frame has an ArraySortData instance that contains all state used -// by the sorting algorithm. The sorting algorithm is implemented as a C++ -// "generator" that can yield to the trampoline to perform a fast JIT => JIT -// call to the comparator function. When the comparator function returns, the -// trampoline calls back into C++ to resume the sorting algorithm. -// -// ArraySortData stores the JS Values in a js::Vector. To ensure we don't leak -// its memory, we have debug assertions to check that for each C++ constructor -// call we call |freeMallocData| exactly once. C++ code calls |freeMallocData| -// when it's done sorting and the JIT exception handler calls it when unwinding -// the trampoline frame. -class ArraySortData { - public: - enum class ComparatorKind : uint8_t { - Unoptimized, - JS, - JSSameRealmNoRectifier, - }; - - static constexpr size_t ComparatorActualArgs = 2; - - // Insertion sort is used if the length is < InsertionSortLimit. - static constexpr size_t InsertionSortLimit = 24; - - using ValueVector = GCVector; - - protected: // Silence Clang warning about unused private fields. - // Data for the comparator call. These fields must match the JitFrameLayout - // to let us perform efficient calls to the comparator from JIT code. - // This is asserted in the JIT trampoline code. - // callArgs[0] is also used to store the return value of the sort function and - // the comparator. - uintptr_t descriptor_; - JSObject* comparator_ = nullptr; - Value thisv; - Value callArgs[ComparatorActualArgs]; - - private: - ValueVector vec; - Value item; - JSContext* cx_; - JSObject* obj_ = nullptr; - - Value* list; - Value* out; - - // The value of the .length property. - uint32_t length; - - // The number of items to sort. Can be less than |length| if the object has - // holes. - uint32_t denseLen; - - uint32_t windowSize; - uint32_t start; - uint32_t mid; - uint32_t end; - uint32_t i, j, k; - - // The state value determines where we resume in sortWithComparator. - enum class State : uint8_t { - Initial, - InsertionSortCall1, - InsertionSortCall2, - MergeSortCall1, - MergeSortCall2 - }; - State state = State::Initial; - ComparatorKind comparatorKind_; - - // Optional padding to ensure proper alignment of the comparator JIT frame. -#if !defined(JS_64BIT) && !defined(DEBUG) - protected: // Silence Clang warning about unused private field. - size_t padding; -#endif - - public: - explicit ArraySortData(JSContext* cx); - - void MOZ_ALWAYS_INLINE init(JSObject* obj, JSObject* comparator, - ValueVector&& vec, uint32_t length, - uint32_t denseLen); - - JSContext* cx() const { return cx_; } - - JSObject* comparator() const { - MOZ_ASSERT(comparator_); - return comparator_; - } - - Value returnValue() const { return callArgs[0]; } - void setReturnValue(JSObject* obj) { callArgs[0].setObject(*obj); } - - Value comparatorArg(size_t index) { - MOZ_ASSERT(index < ComparatorActualArgs); - return callArgs[index]; - } - Value comparatorThisValue() const { return thisv; } - Value comparatorReturnValue() const { return callArgs[0]; } - void setComparatorArgs(const Value& x, const Value& y) { - callArgs[0] = x; - callArgs[1] = y; - } - void setComparatorReturnValue(const Value& v) { callArgs[0] = v; } - - ComparatorKind comparatorKind() const { return comparatorKind_; } - - static ArraySortResult sortWithComparator(ArraySortData* d); - - void freeMallocData(); - void trace(JSTracer* trc); - - static constexpr int32_t offsetOfDescriptor() { - return offsetof(ArraySortData, descriptor_); - } - static constexpr int32_t offsetOfComparator() { - return offsetof(ArraySortData, comparator_); - } - static constexpr int32_t offsetOfComparatorReturnValue() { - return offsetof(ArraySortData, callArgs[0]); - } - static constexpr int32_t offsetOfComparatorThis() { - return offsetof(ArraySortData, thisv); - } - static constexpr int32_t offsetOfComparatorArgs() { - return offsetof(ArraySortData, callArgs); - } -}; - extern ArraySortResult ArraySortFromJit( JSContext* cx, jit::TrampolineNativeFrameLayout* frame); diff --git a/js/src/builtin/AtomicsObject.cpp b/js/src/builtin/AtomicsObject.cpp index c4a221aab8..232108872c 100644 --- a/js/src/builtin/AtomicsObject.cpp +++ b/js/src/builtin/AtomicsObject.cpp @@ -280,6 +280,7 @@ bool AtomicAccess(JSContext* cx, HandleValue obj, HandleValue index, Op op) { return op(ArrayOps{}, unwrappedTypedArray, intIndex); case Scalar::BigUint64: return op(ArrayOps{}, unwrappedTypedArray, intIndex); + case Scalar::Float16: case Scalar::Float32: case Scalar::Float64: case Scalar::Uint8Clamped: @@ -931,7 +932,7 @@ FutexThread::WaitResult js::FutexThread::wait( // See explanation below. if (state_ == WaitingInterrupted) { - UnlockGuard unlock(locked); + UnlockGuard unlock(locked); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ATOMICS_WAIT_NOT_ALLOWED); return WaitResult::Error; @@ -1032,7 +1033,7 @@ FutexThread::WaitResult js::FutexThread::wait( state_ = WaitingInterrupted; { - UnlockGuard unlock(locked); + UnlockGuard unlock(locked); if (!cx->handleInterrupt()) { return WaitResult::Error; } diff --git a/js/src/builtin/Boolean-inl.h b/js/src/builtin/Boolean-inl.h index dca2de84f5..5463f80b57 100644 --- a/js/src/builtin/Boolean-inl.h +++ b/js/src/builtin/Boolean-inl.h @@ -24,8 +24,7 @@ inline bool EmulatesUndefined(JSObject* obj) { return actual->getClass()->emulatesUndefined(); } -inline bool EmulatesUndefinedCheckFuse(JSObject* obj, - size_t fuseValue) { +inline bool EmulatesUndefinedCheckFuse(JSObject* obj, size_t fuseValue) { // This may be called off the main thread. It's OK not to expose the object // here as it doesn't escape. AutoUnsafeCallWithABI unsafe(UnsafeABIStrictness::AllowPendingExceptions); diff --git a/js/src/builtin/DataViewObject.cpp b/js/src/builtin/DataViewObject.cpp index 425fdce51d..9c3e8edcc6 100644 --- a/js/src/builtin/DataViewObject.cpp +++ b/js/src/builtin/DataViewObject.cpp @@ -27,6 +27,7 @@ #include "util/DifferentialTesting.h" #include "vm/ArrayBufferObject.h" #include "vm/Compartment.h" +#include "vm/Float16.h" #include "vm/GlobalObject.h" #include "vm/Interpreter.h" #include "vm/JSContext.h" @@ -416,6 +417,16 @@ NativeType DataViewObject::read(uint64_t offset, size_t length, return val; } +#ifdef NIGHTLY_BUILD +template <> +float16 DataViewObject::read(uint64_t offset, size_t length, + bool isLittleEndian) { + float16 val{}; + val.val = read(offset, length, isLittleEndian); + return val; +} +#endif + template uint32_t DataViewObject::read(uint64_t offset, size_t length, bool isLittleEndian); @@ -499,6 +510,19 @@ inline bool WebIDLCast(JSContext* cx, HandleValue value, return true; } +#ifdef NIGHTLY_BUILD +template <> +inline bool WebIDLCast(JSContext* cx, HandleValue value, + float16* out) { + double temp; + if (!ToNumber(cx, value, &temp)) { + return false; + } + *out = float16(temp); + return true; +} +#endif + template <> inline bool WebIDLCast(JSContext* cx, HandleValue value, float* out) { double temp; @@ -537,7 +561,11 @@ bool DataViewObject::write(JSContext* cx, Handle obj, // See the comment in ElementSpecific::doubleToNative. if (js::SupportDifferentialTesting() && TypeIsFloatingPoint()) { - value = JS::CanonicalizeNaN(value); + if constexpr (std::is_same_v) { + value = JS::CanonicalizeNaN(value.toDouble()); + } else { + value = JS::CanonicalizeNaN(value); + } } // Step 6. @@ -739,6 +767,28 @@ bool DataViewObject::fun_getBigUint64(JSContext* cx, unsigned argc, Value* vp) { return CallNonGenericMethod(cx, args); } +#ifdef NIGHTLY_BUILD +bool DataViewObject::getFloat16Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsDataView(args.thisv())); + + Rooted thisView( + cx, &args.thisv().toObject().as()); + + float16 val{}; + if (!read(cx, thisView, args, &val)) { + return false; + } + + args.rval().setDouble(CanonicalizeNaN(val.toDouble())); + return true; +} + +bool DataViewObject::fun_getFloat16(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} +#endif + bool DataViewObject::getFloat32Impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsDataView(args.thisv())); @@ -927,6 +977,26 @@ bool DataViewObject::fun_setBigUint64(JSContext* cx, unsigned argc, Value* vp) { return CallNonGenericMethod(cx, args); } +#ifdef NIGHTLY_BUILD +bool DataViewObject::setFloat16Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsDataView(args.thisv())); + + Rooted thisView( + cx, &args.thisv().toObject().as()); + + if (!write(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setFloat16(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} +#endif + bool DataViewObject::setFloat32Impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsDataView(args.thisv())); @@ -1083,6 +1153,10 @@ const JSFunctionSpec DataViewObject::methods[] = { DataViewGetInt32), JS_INLINABLE_FN("getUint32", DataViewObject::fun_getUint32, 1, 0, DataViewGetUint32), +#ifdef NIGHTLY_BUILD + // TODO: See Bug 1835034 for JIT support for Float16Array + JS_FN("getFloat16", DataViewObject::fun_getFloat16, 1, 0), +#endif JS_INLINABLE_FN("getFloat32", DataViewObject::fun_getFloat32, 1, 0, DataViewGetFloat32), JS_INLINABLE_FN("getFloat64", DataViewObject::fun_getFloat64, 1, 0, @@ -1103,6 +1177,10 @@ const JSFunctionSpec DataViewObject::methods[] = { DataViewSetInt32), JS_INLINABLE_FN("setUint32", DataViewObject::fun_setUint32, 2, 0, DataViewSetUint32), +#ifdef NIGHTLY_BUILD + // TODO: See Bug 1835034 for JIT support for Float16Array + JS_FN("setFloat16", DataViewObject::fun_setFloat16, 2, 0), +#endif JS_INLINABLE_FN("setFloat32", DataViewObject::fun_setFloat32, 2, 0, DataViewSetFloat32), JS_INLINABLE_FN("setFloat64", DataViewObject::fun_setFloat64, 2, 0, diff --git a/js/src/builtin/DataViewObject.h b/js/src/builtin/DataViewObject.h index db134a5696..aa036cf1c6 100644 --- a/js/src/builtin/DataViewObject.h +++ b/js/src/builtin/DataViewObject.h @@ -115,6 +115,9 @@ class DataViewObject : public ArrayBufferViewObject { static bool getBigUint64Impl(JSContext* cx, const CallArgs& args); static bool fun_getBigUint64(JSContext* cx, unsigned argc, Value* vp); + static bool getFloat16Impl(JSContext* cx, const CallArgs& args); + static bool fun_getFloat16(JSContext* cx, unsigned argc, Value* vp); + static bool getFloat32Impl(JSContext* cx, const CallArgs& args); static bool fun_getFloat32(JSContext* cx, unsigned argc, Value* vp); @@ -145,6 +148,9 @@ class DataViewObject : public ArrayBufferViewObject { static bool setBigUint64Impl(JSContext* cx, const CallArgs& args); static bool fun_setBigUint64(JSContext* cx, unsigned argc, Value* vp); + static bool setFloat16Impl(JSContext* cx, const CallArgs& args); + static bool fun_setFloat16(JSContext* cx, unsigned argc, Value* vp); + static bool setFloat32Impl(JSContext* cx, const CallArgs& args); static bool fun_setFloat32(JSContext* cx, unsigned argc, Value* vp); diff --git a/js/src/builtin/Eval.cpp b/js/src/builtin/Eval.cpp index 0cff2486ad..5fffed2140 100644 --- a/js/src/builtin/Eval.cpp +++ b/js/src/builtin/Eval.cpp @@ -70,7 +70,7 @@ static bool IsEvalCacheCandidate(JSScript* script) { /* static */ HashNumber EvalCacheHashPolicy::hash(const EvalCacheLookup& l) { HashNumber hash = HashStringChars(l.str); - return AddToHash(hash, l.callerScript.get(), l.pc); + return AddToHash(hash, l.callerScript, l.pc); } /* static */ @@ -82,13 +82,18 @@ bool EvalCacheHashPolicy::match(const EvalCacheEntry& cacheEntry, cacheEntry.callerScript == l.callerScript && cacheEntry.pc == l.pc; } +void EvalCacheLookup::trace(JSTracer* trc) { + TraceNullableRoot(trc, &str, "EvalCacheLookup::str"); + TraceNullableRoot(trc, &callerScript, "EvalCacheLookup::callerScript"); +} + // Add the script to the eval cache when EvalKernel is finished class EvalScriptGuard { JSContext* cx_; Rooted script_; /* These fields are only valid if lookup_.str is non-nullptr. */ - EvalCacheLookup lookup_; + Rooted lookup_; mozilla::Maybe> p_; Rooted lookupStr_; @@ -100,12 +105,13 @@ class EvalScriptGuard { ~EvalScriptGuard() { if (script_ && !cx_->isExceptionPending()) { script_->cacheForEval(); - EvalCacheEntry cacheEntry = {lookupStr_, script_, lookup_.callerScript, - lookup_.pc}; - lookup_.str = lookupStr_; - if (lookup_.str && IsEvalCacheCandidate(script_)) { + EvalCacheLookup& lookup = lookup_.get(); + EvalCacheEntry cacheEntry = {lookupStr_, script_, lookup.callerScript, + lookup.pc}; + lookup.str = lookupStr_; + if (lookup.str && IsEvalCacheCandidate(script_)) { // Ignore failure to add cache entry. - if (!p_->add(cx_, cx_->caches().evalCache, lookup_, cacheEntry)) { + if (!p_->add(cx_, cx_->caches().evalCache, lookup, cacheEntry)) { cx_->recoverFromOutOfMemory(); } } @@ -115,13 +121,14 @@ class EvalScriptGuard { void lookupInEvalCache(JSLinearString* str, JSScript* callerScript, jsbytecode* pc) { lookupStr_ = str; - lookup_.str = str; - lookup_.callerScript = callerScript; - lookup_.pc = pc; - p_.emplace(cx_, cx_->caches().evalCache, lookup_); + EvalCacheLookup& lookup = lookup_.get(); + lookup.str = str; + lookup.callerScript = callerScript; + lookup.pc = pc; + p_.emplace(cx_, cx_->caches().evalCache, lookup); if (*p_) { script_ = (*p_)->script; - p_->remove(cx_, cx_->caches().evalCache, lookup_); + p_->remove(cx_, cx_->caches().evalCache, lookup); } } @@ -531,16 +538,11 @@ JS_PUBLIC_API bool JS::IsJSMEnvironment(JSObject* obj) { #ifdef JSGC_HASH_TABLE_CHECKS void RuntimeCaches::checkEvalCacheAfterMinorGC() { - JSContext* cx = TlsContext.get(); - for (auto r = evalCache.all(); !r.empty(); r.popFront()) { - const EvalCacheEntry& entry = r.front(); + gc::CheckTableAfterMovingGC(evalCache, [](const auto& entry) { CheckGCThingAfterMovingGC(entry.str); - EvalCacheLookup lookup(cx); - lookup.str = entry.str; - lookup.callerScript = entry.callerScript; - lookup.pc = entry.pc; - auto ptr = evalCache.lookup(lookup); - MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front()); - } + CheckGCThingAfterMovingGC(entry.script); + CheckGCThingAfterMovingGC(entry.callerScript); + return EvalCacheLookup(entry.str, entry.callerScript, entry.pc); + }); } #endif diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp index cd4ae7f46a..52f60721ea 100644 --- a/js/src/builtin/MapObject.cpp +++ b/js/src/builtin/MapObject.cpp @@ -1384,7 +1384,6 @@ const JSFunctionSpec SetObject::methods[] = { JS_FN("entries", entries, 0, 0), JS_FN("clear", clear, 0, 0), JS_SELF_HOSTED_FN("forEach", "SetForEach", 2, 0), -#ifdef NIGHTLY_BUILD JS_SELF_HOSTED_FN("union", "SetUnion", 1, 0), JS_SELF_HOSTED_FN("difference", "SetDifference", 1, 0), JS_SELF_HOSTED_FN("intersection", "SetIntersection", 1, 0), @@ -1392,7 +1391,6 @@ const JSFunctionSpec SetObject::methods[] = { JS_SELF_HOSTED_FN("isSubsetOf", "SetIsSubsetOf", 1, 0), JS_SELF_HOSTED_FN("isSupersetOf", "SetIsSupersetOf", 1, 0), JS_SELF_HOSTED_FN("isDisjointFrom", "SetIsDisjointFrom", 1, 0), -#endif JS_FN("values", values, 0, 0), // @@iterator and |keys| re-defined in finishInit so that they have the // same identity as |values|. diff --git a/js/src/builtin/ModuleObject.cpp b/js/src/builtin/ModuleObject.cpp index 0365f744a6..251e8b46ff 100644 --- a/js/src/builtin/ModuleObject.cpp +++ b/js/src/builtin/ModuleObject.cpp @@ -176,6 +176,17 @@ ResolvedBindingObject* ResolvedBindingObject::create( return self; } +/////////////////////////////////////////////////////////////////////////// +// ImportAttribute + +ImportAttribute::ImportAttribute(Handle key, Handle value) + : key_(key), value_(value) {} + +void ImportAttribute::trace(JSTracer* trc) { + TraceNullableEdge(trc, &key_, "ImportAttribute::key_"); + TraceNullableEdge(trc, &value_, "ImportAttribute::value_"); +} + /////////////////////////////////////////////////////////////////////////// // ModuleRequestObject /* static */ const JSClass ModuleRequestObject::class_ = { @@ -185,13 +196,15 @@ ResolvedBindingObject* ResolvedBindingObject::create( DEFINE_ATOM_OR_NULL_ACCESSOR_METHOD(ModuleRequestObject, specifier, SpecifierSlot) -ArrayObject* ModuleRequestObject::attributes() const { - JSObject* obj = getReservedSlot(AttributesSlot).toObjectOrNull(); - if (!obj) { - return nullptr; +Span ModuleRequestObject::attributes() const { + Value value = getReservedSlot(AttributesSlot); + if (value.isNullOrUndefined()) { + return Span(); } - - return &obj->as(); + void* ptr = value.toPrivate(); + MOZ_ASSERT(ptr); + auto* vector = static_cast(ptr); + return *vector; } bool ModuleRequestObject::hasAttributes() const { @@ -208,32 +221,22 @@ bool ModuleRequestObject::getModuleType( return true; } - Rooted attributesArray(cx, moduleRequest->attributes()); - RootedObject attributeObject(cx); - RootedId typeId(cx, NameToId(cx->names().type)); - RootedValue value(cx); - - uint32_t numberOfAttributes = attributesArray->length(); - for (uint32_t i = 0; i < numberOfAttributes; i++) { - attributeObject = &attributesArray->getDenseElement(i).toObject(); - - if (!GetProperty(cx, attributeObject, attributeObject, typeId, &value)) { - continue; - } + for (const ImportAttribute& importAttribute : moduleRequest->attributes()) { + if (importAttribute.key() == cx->names().type) { + int32_t isJsonString; + if (!js::CompareStrings(cx, cx->names().json, importAttribute.value(), + &isJsonString)) { + return false; + } - int32_t isJsonString; - if (!js::CompareStrings(cx, cx->names().json, value.toString(), - &isJsonString)) { - return false; - } + if (isJsonString == 0) { + moduleType = JS::ModuleType::JSON; + return true; + } - if (isJsonString == 0) { - moduleType = JS::ModuleType::JSON; + moduleType = JS::ModuleType::Unknown; return true; } - - moduleType = JS::ModuleType::Unknown; - return true; } moduleType = JS::ModuleType::JavaScript; @@ -248,7 +251,7 @@ bool ModuleRequestObject::isInstance(HandleValue value) { /* static */ ModuleRequestObject* ModuleRequestObject::create( JSContext* cx, Handle specifier, - Handle maybeAttributes) { + MutableHandle> maybeAttributes) { ModuleRequestObject* self = NewObjectWithGivenProto(cx, nullptr); if (!self) { @@ -256,7 +259,12 @@ ModuleRequestObject* ModuleRequestObject::create( } self->initReservedSlot(SpecifierSlot, StringOrNullValue(specifier)); - self->initReservedSlot(AttributesSlot, ObjectOrNullValue(maybeAttributes)); + + if (maybeAttributes) { + InitReservedSlot(self, AttributesSlot, maybeAttributes.get().release(), + MemoryUse::ModuleImportAttributes); + } + return self; } @@ -1589,39 +1597,28 @@ bool frontend::StencilModuleMetadata::createModuleRequestObjects( ModuleRequestObject* frontend::StencilModuleMetadata::createModuleRequestObject( JSContext* cx, CompilationAtomCache& atomCache, const StencilModuleRequest& request) const { - Rooted assertionArray(cx); - uint32_t numberOfAssertions = request.assertions.length(); - if (numberOfAssertions > 0) { - assertionArray = NewDenseFullyAllocatedArray(cx, numberOfAssertions); - if (!assertionArray) { + uint32_t numberOfAttributes = request.attributes.length(); + + Rooted> attributes(cx); + if (numberOfAttributes > 0) { + attributes = cx->make_unique(); + if (!attributes) { + ReportOutOfMemory(cx); return nullptr; } - assertionArray->ensureDenseInitializedLength(0, numberOfAssertions); - - Rooted assertionObject(cx); - RootedId assertionKey(cx); - RootedValue assertionValue(cx); - for (uint32_t j = 0; j < numberOfAssertions; ++j) { - assertionObject = NewPlainObject(cx); - if (!assertionObject) { - return nullptr; - } - JSAtom* jsatom = - atomCache.getExistingAtomAt(cx, request.assertions[j].key); - MOZ_ASSERT(jsatom); - assertionKey = AtomToId(jsatom); - - jsatom = atomCache.getExistingAtomAt(cx, request.assertions[j].value); - MOZ_ASSERT(jsatom); - assertionValue = StringValue(jsatom); - - if (!DefineDataProperty(cx, assertionObject, assertionKey, assertionValue, - JSPROP_ENUMERATE)) { - return nullptr; - } + if (!attributes->reserve(numberOfAttributes)) { + ReportOutOfMemory(cx); + return nullptr; + } - assertionArray->initDenseElement(j, ObjectValue(*assertionObject)); + Rooted attributeKey(cx); + Rooted attributeValue(cx); + for (uint32_t j = 0; j < numberOfAttributes; ++j) { + attributeKey = atomCache.getExistingAtomAt(cx, request.attributes[j].key); + attributeValue = + atomCache.getExistingAtomAt(cx, request.attributes[j].value); + attributes->infallibleEmplaceBack(attributeKey, attributeValue); } } @@ -1629,7 +1626,7 @@ ModuleRequestObject* frontend::StencilModuleMetadata::createModuleRequestObject( atomCache.getExistingAtomAt(cx, request.specifier)); MOZ_ASSERT(specifier); - return ModuleRequestObject::create(cx, specifier, assertionArray); + return ModuleRequestObject::create(cx, specifier, &attributes); } bool frontend::StencilModuleMetadata::createImportEntries( @@ -1796,34 +1793,24 @@ bool frontend::StencilModuleMetadata::initModule( return true; } -bool ModuleBuilder::isAssertionSupported(frontend::TaggedParserAtomIndex key) { - if (!key.isWellKnownAtomId()) { - return false; - } - - return key.toWellKnownAtomId() == WellKnownAtomId::type; -} - -bool ModuleBuilder::processAssertions(frontend::StencilModuleRequest& request, - frontend::ListNode* assertionList) { +bool ModuleBuilder::processAttributes(frontend::StencilModuleRequest& request, + frontend::ListNode* attributeList) { using namespace js::frontend; - for (ParseNode* assertionItem : assertionList->contents()) { - BinaryNode* assertion = &assertionItem->as(); - MOZ_ASSERT(assertion->isKind(ParseNodeKind::ImportAttribute)); + for (ParseNode* attributeItem : attributeList->contents()) { + BinaryNode* attribute = &attributeItem->as(); + MOZ_ASSERT(attribute->isKind(ParseNodeKind::ImportAttribute)); - auto key = assertion->left()->as().atom(); - auto value = assertion->right()->as().atom(); + auto key = attribute->left()->as().atom(); + auto value = attribute->right()->as().atom(); - if (isAssertionSupported(key)) { - markUsedByStencil(key); - markUsedByStencil(value); + markUsedByStencil(key); + markUsedByStencil(value); - StencilModuleAssertion assertionStencil(key, value); - if (!request.assertions.append(assertionStencil)) { - js::ReportOutOfMemory(fc_); - return false; - } + StencilModuleImportAttribute attributeStencil(key, value); + if (!request.attributes.append(attributeStencil)) { + js::ReportOutOfMemory(fc_); + return false; } } @@ -1844,12 +1831,12 @@ bool ModuleBuilder::processImport(frontend::BinaryNode* importNode) { auto* moduleSpec = &moduleRequest->left()->as(); MOZ_ASSERT(moduleSpec->isKind(ParseNodeKind::StringExpr)); - auto* assertionList = &moduleRequest->right()->as(); - MOZ_ASSERT(assertionList->isKind(ParseNodeKind::ImportAttributeList)); + auto* attributeList = &moduleRequest->right()->as(); + MOZ_ASSERT(attributeList->isKind(ParseNodeKind::ImportAttributeList)); auto specifier = moduleSpec->atom(); MaybeModuleRequestIndex moduleRequestIndex = - appendModuleRequest(specifier, assertionList); + appendModuleRequest(specifier, attributeList); if (!moduleRequestIndex.isSome()) { return false; } @@ -2089,12 +2076,12 @@ bool ModuleBuilder::processExportFrom(frontend::BinaryNode* exportNode) { auto* moduleSpec = &moduleRequest->left()->as(); MOZ_ASSERT(moduleSpec->isKind(ParseNodeKind::StringExpr)); - auto* assertionList = &moduleRequest->right()->as(); - MOZ_ASSERT(assertionList->isKind(ParseNodeKind::ImportAttributeList)); + auto* attributeList = &moduleRequest->right()->as(); + MOZ_ASSERT(attributeList->isKind(ParseNodeKind::ImportAttributeList)); auto specifier = moduleSpec->atom(); MaybeModuleRequestIndex moduleRequestIndex = - appendModuleRequest(specifier, assertionList); + appendModuleRequest(specifier, attributeList); if (!moduleRequestIndex.isSome()) { return false; } @@ -2192,11 +2179,11 @@ bool ModuleBuilder::appendExportEntry( frontend::MaybeModuleRequestIndex ModuleBuilder::appendModuleRequest( frontend::TaggedParserAtomIndex specifier, - frontend::ListNode* assertionList) { + frontend::ListNode* attributeList) { markUsedByStencil(specifier); auto request = frontend::StencilModuleRequest(specifier); - if (!processAssertions(request, assertionList)) { + if (!processAttributes(request, attributeList)) { return MaybeModuleRequestIndex(); } @@ -2304,17 +2291,17 @@ bool ModuleObject::topLevelCapabilityReject(JSContext* cx, return AsyncFunctionThrown(cx, promise, error); } -// https://tc39.es/proposal-import-assertions/#sec-evaluate-import-call +// https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call // NOTE: The caller needs to handle the promise. static bool EvaluateDynamicImportOptions( JSContext* cx, HandleValue optionsArg, - MutableHandle assertionArrayArg) { - // Step 10. If options is not undefined, then. + MutableHandle attributesArrayArg) { + // Step 11. If options is not undefined, then if (optionsArg.isUndefined()) { return true; } - // Step 10.a. If Type(options) is not Object, + // Step 11.a. If options is not an Object, then if (!optionsArg.isObject()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "import", @@ -2322,114 +2309,124 @@ static bool EvaluateDynamicImportOptions( return false; } - RootedObject assertWrapperObject(cx, &optionsArg.toObject()); - RootedValue assertValue(cx); + RootedObject attributesWrapperObject(cx, &optionsArg.toObject()); + RootedValue attributesValue(cx); - // Step 10.b. Let attributesObj be Completion(Get(options, "with")). + // Step 11.b. Let attributesObj be Completion(Get(options, "with")). RootedId withId(cx, NameToId(cx->names().with)); - if (!GetProperty(cx, assertWrapperObject, assertWrapperObject, withId, - &assertValue)) { + if (!GetProperty(cx, attributesWrapperObject, attributesWrapperObject, withId, + &attributesValue)) { return false; } - if (assertValue.isUndefined() && + // Step 11.d. If the host supports the deprecated assert keyword for import + // attributes and attributesObj is undefined, then + if (attributesValue.isUndefined() && cx->options().importAttributesAssertSyntax()) { - // Step 10.b. Let assertionsObj be Get(options, "assert"). + // Step 11.d.i. Set attributesObj to Completion(Get(options, "assert")). RootedId assertId(cx, NameToId(cx->names().assert_)); - if (!GetProperty(cx, assertWrapperObject, assertWrapperObject, assertId, - &assertValue)) { + if (!GetProperty(cx, attributesWrapperObject, attributesWrapperObject, + assertId, &attributesValue)) { return false; } } - // Step 10.d. If assertionsObj is not undefined. - if (assertValue.isUndefined()) { + // Step 11.e. If attributesObj is not undefined, then + if (attributesValue.isUndefined()) { return true; } - // Step 10.d.i. If Type(assertionsObj) is not Object. - if (!assertValue.isObject()) { + // Step 11.e.i. If attributesObj is not an Object, then + if (!attributesValue.isObject()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "import", - "object or undefined", InformalValueTypeName(assertValue)); + "object or undefined", InformalValueTypeName(attributesValue)); return false; } - // Step 10.d.i. Let keys be EnumerableOwnPropertyNames(assertionsObj, key). - RootedObject assertObject(cx, &assertValue.toObject()); - RootedIdVector assertions(cx); - if (!GetPropertyKeys(cx, assertObject, JSITER_OWNONLY, &assertions)) { + // Step 11.e.ii. Let entries be + // Completion(EnumerableOwnProperties(attributesObj, key+value)). + RootedObject attributesObject(cx, &attributesValue.toObject()); + RootedIdVector attributes(cx); + if (!GetPropertyKeys(cx, attributesObject, JSITER_OWNONLY, &attributes)) { return false; } - uint32_t numberOfAssertions = assertions.length(); - if (numberOfAssertions == 0) { + uint32_t numberOfAttributes = attributes.length(); + if (numberOfAttributes == 0) { return true; } - // Step 9 (reordered). Let assertions be a new empty List. - Rooted assertionArray( - cx, NewDenseFullyAllocatedArray(cx, numberOfAssertions)); - if (!assertionArray) { + // Step 10 (reordered). Let attributes be a new empty List. + if (!attributesArrayArg.reserve(numberOfAttributes)) { + ReportOutOfMemory(cx); return false; } - assertionArray->ensureDenseInitializedLength(0, numberOfAssertions); - // Step 10.d.iv. Let supportedAssertions be - // !HostGetSupportedImportAssertions(). - // Note: This should be driven by a host hook, howver the infrastructure of - // said host hook is deeply unclear, and so right now embedders will - // not have the ability to alter or extend the set of supported - // assertion types. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1840723. - size_t numberOfValidAssertions = 0; + size_t numberOfValidAttributes = 0; - // Step 10.d.v. For each String key of keys, + // Step 11.e.iv. For each element entry of entries, do RootedId key(cx); - for (size_t i = 0; i < numberOfAssertions; i++) { - key = assertions[i]; - - // Step 10.d.v.1. Let value be Get(assertionsObj, key). - RootedValue value(cx); - if (!GetProperty(cx, assertObject, assertObject, key, &value)) { - return false; - } - - // Step 10.d.v.3. If Type(value) is not String, then. - if (!value.isString()) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_NOT_EXPECTED_TYPE, "import", "string", - InformalValueTypeName(value)); + RootedValue value(cx); + Rooted keyAtom(cx); + Rooted valueString(cx); + for (size_t i = 0; i < numberOfAttributes; i++) { + // Step 11.e.ii.iv.1. Let key be ! Get(entry, "0"). + key = attributes[i]; + + // Step 11.e.ii.iv.2. Let value be ! Get(entry, "1"). + if (!GetProperty(cx, attributesObject, attributesObject, key, &value)) { return false; } - // Step 10.d.v.4. If supportedAssertions contains key, then Append { - // [[Key]]: key, [[Value]]: value } to assertions. - // Note: We only currently support the "type" assertion; this will need - // extension - bool supported = key.isAtom() ? key.toAtom() == cx->names().type : false; - if (supported) { - Rooted assertionObj(cx, NewPlainObject(cx)); - if (!assertionObj) { + // Step 11.e.ii.iv.3. If key is a String, then + if (key.isString()) { + // Step 11.f (reordered). If AllImportAttributesSupported(attributes) is + // false, then + // + // Note: This should be driven by a host hook + // (HostGetSupportedImportAttributes), however the infrastructure of said + // host hook is deeply unclear, and so right now embedders will not have + // the ability to alter or extend the set of supported attributes. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1840723. + bool supported = key.isAtom(cx->names().type); + if (!supported) { + UniqueChars printableKey = AtomToPrintableString(cx, key.toAtom()); + if (!printableKey) { + return false; + } + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_IMPORT_ATTRIBUTES_UNSUPPORTED_ATTRIBUTE, + printableKey.get()); return false; } - if (!DefineDataProperty(cx, assertionObj, key, value, JSPROP_ENUMERATE)) { + // Step 10.d.v.3.a. If value is not a String, then + if (!value.isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "import", "string", + InformalValueTypeName(value)); return false; } - assertionArray->initDenseElement(numberOfValidAssertions, - ObjectValue(*assertionObj)); - ++numberOfValidAssertions; + // Step 10.d.v.3.b. Append the ImportAttribute Record { [[Key]]: key, + // [[Value]]: value } to attributes. + keyAtom = key.toAtom(); + valueString = value.toString(); + attributesArrayArg.infallibleEmplaceBack(keyAtom, valueString); + ++numberOfValidAttributes; } } - if (numberOfValidAssertions == 0) { + if (numberOfValidAttributes == 0) { return true; } - assertionArray->setLength(numberOfValidAssertions); - assertionArrayArg.set(assertionArray); + // Step 10.g (skipped). Sort attributes according to the lexicographic order + // of their [[Key]] fields, treating the value of each such field as a + // sequence of UTF-16 code unit values. + // + // We only support "type", so we can ignore this. return true; } @@ -2482,16 +2479,32 @@ JSObject* js::StartDynamicModuleImport(JSContext* cx, HandleScript script, return promise; } - Rooted assertionArray(cx); - if (!EvaluateDynamicImportOptions(cx, optionsArg, &assertionArray)) { + Rooted tempAttributes(cx); + if (!EvaluateDynamicImportOptions(cx, optionsArg, &tempAttributes)) { if (!RejectPromiseWithPendingError(cx, promise)) { return nullptr; } return promise; } + Rooted> attributes(cx); + if (!tempAttributes.empty()) { + attributes = cx->make_unique(); + if (!attributes) { + return nullptr; + } + if (!attributes->reserve(tempAttributes.length())) { + ReportOutOfMemory(cx); + return nullptr; + } + if (!attributes->appendAll(tempAttributes)) { + ReportOutOfMemory(cx); + return nullptr; + } + } + RootedObject moduleRequest( - cx, ModuleRequestObject::create(cx, specifierAtom, assertionArray)); + cx, ModuleRequestObject::create(cx, specifierAtom, &attributes)); if (!moduleRequest) { if (!RejectPromiseWithPendingError(cx, promise)) { return nullptr; @@ -2669,8 +2682,9 @@ static bool OnResolvedDynamicModule(JSContext* cx, unsigned argc, Value* vp) { } Rooted promise(cx, TargetFromHandler(args)); + Rooted> attributes(cx); RootedObject moduleRequest( - cx, ModuleRequestObject::create(cx, specifier, nullptr)); + cx, ModuleRequestObject::create(cx, specifier, &attributes)); if (!moduleRequest) { return RejectPromiseWithPendingError(cx, promise); } diff --git a/js/src/builtin/ModuleObject.h b/js/src/builtin/ModuleObject.h index cb74d67c40..3533201675 100644 --- a/js/src/builtin/ModuleObject.h +++ b/js/src/builtin/ModuleObject.h @@ -50,6 +50,21 @@ class ModuleObject; class PromiseObject; class ScriptSourceObject; +class ImportAttribute { + const HeapPtr key_; + const HeapPtr value_; + + public: + ImportAttribute(Handle key, Handle value); + + JSAtom* key() const { return key_; } + JSString* value() const { return value_; } + + void trace(JSTracer* trc); +}; + +using ImportAttributeVector = GCVector; + class ModuleRequestObject : public NativeObject { public: enum { SpecifierSlot = 0, AttributesSlot, SlotCount }; @@ -58,10 +73,10 @@ class ModuleRequestObject : public NativeObject { static bool isInstance(HandleValue value); [[nodiscard]] static ModuleRequestObject* create( JSContext* cx, Handle specifier, - Handle maybeAttributes); + MutableHandle> maybeAttributes); JSAtom* specifier() const; - ArrayObject* attributes() const; + mozilla::Span attributes() const; bool hasAttributes() const; static bool getModuleType(JSContext* cx, const Handle moduleRequest, diff --git a/js/src/builtin/ReflectParse.cpp b/js/src/builtin/ReflectParse.cpp index dde953143e..aa5666f238 100644 --- a/js/src/builtin/ReflectParse.cpp +++ b/js/src/builtin/ReflectParse.cpp @@ -505,7 +505,7 @@ class NodeBuilder { [[nodiscard]] bool debuggerStatement(TokenPos* pos, MutableHandleValue dst); [[nodiscard]] bool moduleRequest(HandleValue moduleSpec, - NodeVector& assertions, TokenPos* pos, + NodeVector& attributes, TokenPos* pos, MutableHandleValue dst); [[nodiscard]] bool importAttribute(HandleValue key, HandleValue value, @@ -1160,10 +1160,10 @@ bool NodeBuilder::yieldExpression(HandleValue arg, YieldKind kind, dst); } -bool NodeBuilder::moduleRequest(HandleValue moduleSpec, NodeVector& assertions, +bool NodeBuilder::moduleRequest(HandleValue moduleSpec, NodeVector& attributes, TokenPos* pos, MutableHandleValue dst) { RootedValue array(cx); - if (!newArray(assertions, &array)) { + if (!newArray(attributes, &array)) { return false; } @@ -1967,12 +1967,12 @@ bool ASTSerializer::exportDeclaration(ParseNode* exportNode, &moduleRequest->as().right()->as(); MOZ_ASSERT(attributeList->isKind(ParseNodeKind::ImportAttributeList)); - NodeVector assertions(cx); - if (!importAttributes(attributeList, assertions)) { + NodeVector attributes(cx); + if (!importAttributes(attributeList, attributes)) { return false; } - if (!builder.moduleRequest(moduleSpec, assertions, &exportNode->pn_pos, + if (!builder.moduleRequest(moduleSpec, attributes, &exportNode->pn_pos, &moduleRequestValue)) { return false; } @@ -2030,13 +2030,13 @@ bool ASTSerializer::importAttributes(ListNode* attributeList, return false; } - RootedValue assertion(cx); + RootedValue attribute(cx); if (!builder.importAttribute(key, value, &attributeNode->pn_pos, - &assertion)) { + &attribute)) { return false; } - if (!attributes.append(assertion)) { + if (!attributes.append(attribute)) { return false; } } diff --git a/js/src/builtin/SelfHostingDefines.h b/js/src/builtin/SelfHostingDefines.h index 0f8fd9694d..a781cc1498 100644 --- a/js/src/builtin/SelfHostingDefines.h +++ b/js/src/builtin/SelfHostingDefines.h @@ -119,6 +119,7 @@ #define TYPEDARRAY_KIND_UINT8CLAMPED 8 #define TYPEDARRAY_KIND_BIGINT64 9 #define TYPEDARRAY_KIND_BIGUINT64 10 +#define TYPEDARRAY_KIND_FLOAT16 11 #define WRAP_FOR_VALID_ITERATOR_ITERATOR_SLOT 0 #define WRAP_FOR_VALID_ITERATOR_NEXT_METHOD_SLOT 1 diff --git a/js/src/builtin/Set.js b/js/src/builtin/Set.js index d1d877ba44..1dbd13cf93 100644 --- a/js/src/builtin/Set.js +++ b/js/src/builtin/Set.js @@ -120,7 +120,6 @@ function SetIteratorNext() { return retVal; } -#ifdef NIGHTLY_BUILD // GetSetRecord ( obj ) // // https://tc39.es/proposal-set-methods/#sec-getsetrecord @@ -564,4 +563,3 @@ function SetIsDisjointFrom(other) { // Step 7. return true; } -#endif diff --git a/js/src/builtin/ShadowRealm.cpp b/js/src/builtin/ShadowRealm.cpp index 0f4efcde14..6c76a2b1b3 100644 --- a/js/src/builtin/ShadowRealm.cpp +++ b/js/src/builtin/ShadowRealm.cpp @@ -444,9 +444,9 @@ static JSObject* ShadowRealmImportValue(JSContext* cx, return promise; } - Rooted assertionArray(cx); + Rooted> attributes(cx); Rooted moduleRequest( - cx, ModuleRequestObject::create(cx, specifierAtom, assertionArray)); + cx, ModuleRequestObject::create(cx, specifierAtom, &attributes)); if (!moduleRequest) { if (!RejectPromiseWithPendingError(cx, promise)) { return nullptr; diff --git a/js/src/builtin/Sorting-inl.h b/js/src/builtin/Sorting-inl.h new file mode 100644 index 0000000000..83217d3e6c --- /dev/null +++ b/js/src/builtin/Sorting-inl.h @@ -0,0 +1,307 @@ +/* -*- 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/. */ + +#ifndef builtin_Sorting_inl_h +#define builtin_Sorting_inl_h + +#include "builtin/Sorting.h" + +#include "js/Conversions.h" +#include "vm/JSContext.h" +#include "vm/JSFunction.h" +#include "vm/JSObject.h" + +namespace js { + +void ArraySortData::init(JSObject* obj, JSObject* comparator, ValueVector&& vec, + uint32_t length, uint32_t denseLen) { + MOZ_ASSERT(!vec.empty(), "must have items to sort"); + MOZ_ASSERT(denseLen <= length); + + obj_ = obj; + comparator_ = comparator; + + this->length = length; + this->denseLen = denseLen; + this->vec = std::move(vec); + + auto getComparatorKind = [](JSContext* cx, JSObject* comparator) { + if (!comparator->is()) { + return ComparatorKind::Unoptimized; + } + JSFunction* fun = &comparator->as(); + if (!fun->hasJitEntry() || fun->isClassConstructor()) { + return ComparatorKind::Unoptimized; + } + if (fun->realm() == cx->realm() && fun->nargs() <= ComparatorActualArgs) { + return ComparatorKind::JSSameRealmNoRectifier; + } + return ComparatorKind::JS; + }; + comparatorKind_ = getComparatorKind(cx(), comparator); +} + +ArraySortData::ArraySortData(JSContext* cx) : cx_(cx) { +#ifdef DEBUG + cx_->liveArraySortDataInstances++; +#endif +} + +void ArraySortData::freeMallocData() { + vec.clearAndFree(); +#ifdef DEBUG + MOZ_ASSERT(cx_->liveArraySortDataInstances > 0); + cx_->liveArraySortDataInstances--; +#endif +} + +template +static MOZ_ALWAYS_INLINE ArraySortResult +MaybeYieldToComparator(ArraySortData* d, const Value& x, const Value& y) { + if constexpr (Kind == ArraySortKind::Array) { + // https://tc39.es/ecma262/#sec-comparearrayelements + // 23.1.3.30.2 CompareArrayElements ( x, y, comparefn ) + + // Steps 1-2. + if (x.isUndefined()) { + d->setComparatorReturnValue(Int32Value(y.isUndefined() ? 0 : 1)); + return ArraySortResult::Done; + } + + // Step 3. + if (y.isUndefined()) { + d->setComparatorReturnValue(Int32Value(-1)); + return ArraySortResult::Done; + } + } else { + // https://tc39.es/ecma262/#sec-comparetypedarrayelements + // 23.2.4.7 CompareTypedArrayElements ( x, y, comparefn ) + + // Step 1. + MOZ_ASSERT((x.isNumber() && y.isNumber()) || + (x.isBigInt() && y.isBigInt())); + } + + // Yield to the JIT trampoline (or js::array_sort) if the comparator is a JS + // function we can call more efficiently from JIT code. + auto kind = d->comparatorKind(); + if (MOZ_LIKELY(kind != ArraySortData::ComparatorKind::Unoptimized)) { + d->setComparatorArgs(x, y); + return (kind == ArraySortData::ComparatorKind::JSSameRealmNoRectifier) + ? ArraySortResult::CallJSSameRealmNoRectifier + : ArraySortResult::CallJS; + } + return CallComparatorSlow(d, x, y); +} + +static MOZ_ALWAYS_INLINE bool RvalIsLessOrEqual(ArraySortData* data, + bool* lessOrEqual) { + // https://tc39.es/ecma262/#sec-comparearrayelements + // 23.1.3.30.2 CompareArrayElements ( x, y, comparefn ) + // + // https://tc39.es/ecma262/#sec-comparetypedarrayelements + // 23.2.4.7 CompareTypedArrayElements ( x, y, comparefn ) + // + // Note: CompareTypedArrayElements step 2 is identical to CompareArrayElements + // step 4. + + // Fast path for int32 return values. + Value rval = data->comparatorReturnValue(); + if (MOZ_LIKELY(rval.isInt32())) { + *lessOrEqual = rval.toInt32() <= 0; + return true; + } + + // Step 4.a. + Rooted rvalRoot(data->cx(), rval); + double d; + if (MOZ_UNLIKELY(!ToNumber(data->cx(), rvalRoot, &d))) { + return false; + } + + // Step 4.b-c. + *lessOrEqual = std::isnan(d) ? true : (d <= 0); + return true; +} + +static MOZ_ALWAYS_INLINE void CopyValues(Value* out, const Value* list, + uint32_t start, uint32_t end) { + for (uint32_t i = start; i <= end; i++) { + out[i] = list[i]; + } +} + +// static +template +ArraySortResult ArraySortData::sortWithComparatorShared(ArraySortData* d) { + auto& vec = d->vec; + + // This function is like a generator that is called repeatedly from the JIT + // trampoline or js::array_sort. Resume the sorting algorithm where we left + // off before calling the comparator. + switch (d->state) { + case State::Initial: + break; + case State::InsertionSortCall1: + goto insertion_sort_call1; + case State::InsertionSortCall2: + goto insertion_sort_call2; + case State::MergeSortCall1: + goto merge_sort_call1; + case State::MergeSortCall2: + goto merge_sort_call2; + } + + d->list = vec.begin(); + + // Use insertion sort for small arrays. + if (d->denseLen <= InsertionSortMaxLength) { + for (d->i = 1; d->i < d->denseLen; d->i++) { + d->item = vec[d->i]; + d->j = d->i - 1; + do { + { + ArraySortResult res = + MaybeYieldToComparator(d, vec[d->j], d->item); + if (res != ArraySortResult::Done) { + d->state = State::InsertionSortCall1; + return res; + } + } + insertion_sort_call1: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + if (lessOrEqual) { + break; + } + vec[d->j + 1] = vec[d->j]; + } while (d->j-- > 0); + vec[d->j + 1] = d->item; + } + } else { + static constexpr size_t InitialWindowSize = 4; + + // Use insertion sort for initial ranges. + for (d->start = 0; d->start < d->denseLen - 1; + d->start += InitialWindowSize) { + d->end = + std::min(d->start + InitialWindowSize - 1, d->denseLen - 1); + for (d->i = d->start + 1; d->i <= d->end; d->i++) { + d->item = vec[d->i]; + d->j = d->i - 1; + do { + { + ArraySortResult res = + MaybeYieldToComparator(d, vec[d->j], d->item); + if (res != ArraySortResult::Done) { + d->state = State::InsertionSortCall2; + return res; + } + } + insertion_sort_call2: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + if (lessOrEqual) { + break; + } + vec[d->j + 1] = vec[d->j]; + } while (d->j-- > d->start); + vec[d->j + 1] = d->item; + } + } + + // Merge sort. Set d->out to scratch space initially. + d->out = vec.begin() + d->denseLen; + for (d->windowSize = InitialWindowSize; d->windowSize < d->denseLen; + d->windowSize *= 2) { + for (d->start = 0; d->start < d->denseLen; + d->start += 2 * d->windowSize) { + // The midpoint between the two subarrays. + d->mid = d->start + d->windowSize - 1; + + // To keep from going over the edge. + d->end = std::min(d->start + 2 * d->windowSize - 1, + d->denseLen - 1); + + // Merge comparator-sorted slices list[start..<=mid] and + // list[mid+1..<=end], storing the merged sequence in out[start..<=end]. + + // Skip lopsided runs to avoid doing useless work. + if (d->mid >= d->end) { + CopyValues(d->out, d->list, d->start, d->end); + continue; + } + + // Skip calling the comparator if the sub-list is already sorted. + { + ArraySortResult res = MaybeYieldToComparator( + d, d->list[d->mid], d->list[d->mid + 1]); + if (res != ArraySortResult::Done) { + d->state = State::MergeSortCall1; + return res; + } + } + merge_sort_call1: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + if (lessOrEqual) { + CopyValues(d->out, d->list, d->start, d->end); + continue; + } + + d->i = d->start; + d->j = d->mid + 1; + d->k = d->start; + + while (d->i <= d->mid && d->j <= d->end) { + { + ArraySortResult res = + MaybeYieldToComparator(d, d->list[d->i], d->list[d->j]); + if (res != ArraySortResult::Done) { + d->state = State::MergeSortCall2; + return res; + } + } + merge_sort_call2: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + d->out[d->k++] = lessOrEqual ? d->list[d->i++] : d->list[d->j++]; + } + + // Empty out any remaining elements. Use local variables to let the + // compiler generate more efficient code. + Value* out = d->out; + Value* list = d->list; + uint32_t k = d->k; + uint32_t mid = d->mid; + uint32_t end = d->end; + for (uint32_t i = d->i; i <= mid; i++) { + out[k++] = list[i]; + } + for (uint32_t j = d->j; j <= end; j++) { + out[k++] = list[j]; + } + } + + // Swap both lists. + std::swap(d->list, d->out); + } + } + + return ArraySortResult::Done; +} + +} // namespace js + +#endif /* builtin_Sorting_inl_h */ diff --git a/js/src/builtin/Sorting.h b/js/src/builtin/Sorting.h new file mode 100644 index 0000000000..4d5216c441 --- /dev/null +++ b/js/src/builtin/Sorting.h @@ -0,0 +1,179 @@ +/* -*- 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/. */ + +#ifndef builtin_Sorting_h +#define builtin_Sorting_h + +#include "mozilla/Attributes.h" + +#include "js/GCVector.h" +#include "vm/JSObject.h" + +// Code used by Array.prototype.sort and %TypedArray%.prototype.sort to sort +// objects based on a user-defined comparator function. + +namespace js { + +// Note: we use uint32_t because the JIT code uses branch32. +enum class ArraySortResult : uint32_t { + Failure, + Done, + CallJS, + CallJSSameRealmNoRectifier +}; + +enum class ArraySortKind { + Array, + TypedArray, +}; + +// We use a JIT trampoline to optimize sorting with a comparator function. The +// trampoline frame has an ArraySortData instance that contains all state used +// by the sorting algorithm. The sorting algorithm is implemented as a C++ +// "generator" that can yield to the trampoline to perform a fast JIT => JIT +// call to the comparator function. When the comparator function returns, the +// trampoline calls back into C++ to resume the sorting algorithm. +// +// ArraySortData stores the JS Values in a js::Vector. To ensure we don't leak +// its memory, we have debug assertions to check that for each C++ constructor +// call we call |freeMallocData| exactly once. C++ code calls |freeMallocData| +// when it's done sorting and the JIT exception handler calls it when unwinding +// the trampoline frame. +class ArraySortData { + public: + enum class ComparatorKind : uint8_t { + Unoptimized, + JS, + JSSameRealmNoRectifier, + }; + + // Insertion sort is used if the length is <= InsertionSortMaxLength. + static constexpr size_t InsertionSortMaxLength = 8; + + static constexpr size_t ComparatorActualArgs = 2; + + using ValueVector = GCVector; + + protected: // Silence Clang warning about unused private fields. + // Data for the comparator call. These fields must match the JitFrameLayout + // to let us perform efficient calls to the comparator from JIT code. + // This is asserted in the JIT trampoline code. + // callArgs[0] is also used to store the return value of the sort function and + // the comparator. + uintptr_t descriptor_; + JSObject* comparator_ = nullptr; + Value thisv; + Value callArgs[ComparatorActualArgs]; + + private: + ValueVector vec; + Value item; + JSContext* cx_; + JSObject* obj_ = nullptr; + + Value* list; + Value* out; + + // The value of the .length property. + uint32_t length; + + // The number of items to sort. Can be less than |length| if the object has + // holes. + uint32_t denseLen; + + uint32_t windowSize; + uint32_t start; + uint32_t mid; + uint32_t end; + uint32_t i, j, k; + + // The state value determines where we resume in sortWithComparator. + enum class State : uint8_t { + Initial, + InsertionSortCall1, + InsertionSortCall2, + MergeSortCall1, + MergeSortCall2 + }; + State state = State::Initial; + ComparatorKind comparatorKind_; + + // Optional padding to ensure proper alignment of the comparator JIT frame. +#if !defined(JS_64BIT) && !defined(DEBUG) + protected: // Silence Clang warning about unused private field. + size_t padding; +#endif + + private: + // Merge sort requires extra scratch space in the vector. Insertion sort + // should be used for short arrays that fit in the vector's inline storage, to + // avoid extra malloc calls. + static_assert(decltype(vec)::InlineLength <= InsertionSortMaxLength); + + template + static MOZ_ALWAYS_INLINE ArraySortResult + sortWithComparatorShared(ArraySortData* d); + + public: + explicit inline ArraySortData(JSContext* cx); + + void MOZ_ALWAYS_INLINE init(JSObject* obj, JSObject* comparator, + ValueVector&& vec, uint32_t length, + uint32_t denseLen); + + JSContext* cx() const { return cx_; } + + JSObject* comparator() const { + MOZ_ASSERT(comparator_); + return comparator_; + } + + Value returnValue() const { return callArgs[0]; } + void setReturnValue(JSObject* obj) { callArgs[0].setObject(*obj); } + + Value comparatorArg(size_t index) { + MOZ_ASSERT(index < ComparatorActualArgs); + return callArgs[index]; + } + Value comparatorThisValue() const { return thisv; } + Value comparatorReturnValue() const { return callArgs[0]; } + void setComparatorArgs(const Value& x, const Value& y) { + callArgs[0] = x; + callArgs[1] = y; + } + void setComparatorReturnValue(const Value& v) { callArgs[0] = v; } + + ComparatorKind comparatorKind() const { return comparatorKind_; } + + static ArraySortResult sortArrayWithComparator(ArraySortData* d); + static ArraySortResult sortTypedArrayWithComparator(ArraySortData* d); + + inline void freeMallocData(); + void trace(JSTracer* trc); + + static constexpr int32_t offsetOfDescriptor() { + return offsetof(ArraySortData, descriptor_); + } + static constexpr int32_t offsetOfComparator() { + return offsetof(ArraySortData, comparator_); + } + static constexpr int32_t offsetOfComparatorReturnValue() { + return offsetof(ArraySortData, callArgs[0]); + } + static constexpr int32_t offsetOfComparatorThis() { + return offsetof(ArraySortData, thisv); + } + static constexpr int32_t offsetOfComparatorArgs() { + return offsetof(ArraySortData, callArgs); + } +}; + +ArraySortResult CallComparatorSlow(ArraySortData* d, const Value& x, + const Value& y); + +} // namespace js + +#endif /* builtin_Sorting_h */ diff --git a/js/src/builtin/Sorting.js b/js/src/builtin/Sorting.js deleted file mode 100644 index 175b506192..0000000000 --- a/js/src/builtin/Sorting.js +++ /dev/null @@ -1,120 +0,0 @@ -/* 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/. */ - -// We use varying sorts across the self-hosted codebase. All sorts are -// consolidated here to avoid confusion and re-implementation of existing -// algorithms. - -// For sorting small typed arrays. -function InsertionSort(array, from, to, comparefn) { - var item, swap, i, j; - for (i = from + 1; i <= to; i++) { - item = array[i]; - for (j = i - 1; j >= from; j--) { - swap = array[j]; - if (callContentFunction(comparefn, undefined, swap, item) <= 0) { - break; - } - array[j + 1] = swap; - } - array[j + 1] = item; - } -} - -// A helper function for MergeSortTypedArray. -// -// Merge comparefn-sorted slices list[start..<=mid] and list[mid+1..<=end], -// storing the merged sequence in out[start..<=end]. -function MergeTypedArray(list, out, start, mid, end, comparefn) { - // Skip lopsided runs to avoid doing useless work. - // Skip calling the comparator if the sub-list is already sorted. - if ( - mid >= end || - callContentFunction(comparefn, undefined, list[mid], list[mid + 1]) <= 0 - ) { - for (var i = start; i <= end; i++) { - out[i] = list[i]; - } - return; - } - - var i = start; - var j = mid + 1; - var k = start; - while (i <= mid && j <= end) { - var lvalue = list[i]; - var rvalue = list[j]; - if (callContentFunction(comparefn, undefined, lvalue, rvalue) <= 0) { - out[k++] = lvalue; - i++; - } else { - out[k++] = rvalue; - j++; - } - } - - // Empty out any remaining elements. - while (i <= mid) { - out[k++] = list[i++]; - } - while (j <= end) { - out[k++] = list[j++]; - } -} - -// Iterative, bottom up, mergesort. Optimized version for TypedArrays. -function MergeSortTypedArray(array, len, comparefn) { - assert( - IsPossiblyWrappedTypedArray(array), - "MergeSortTypedArray works only with typed arrays." - ); - - // Use the same TypedArray kind for the buffer. - var C = ConstructorForTypedArray(array); - - var lBuffer = new C(len); - - // Copy all elements into a temporary buffer, so that any modifications - // when calling |comparefn| are ignored. - for (var i = 0; i < len; i++) { - lBuffer[i] = array[i]; - } - - // Insertion sort for small arrays, where "small" is defined by performance - // testing. - if (len < 8) { - InsertionSort(lBuffer, 0, len - 1, comparefn); - - return lBuffer; - } - - // We do all of our allocating up front. - var rBuffer = new C(len); - - // Use insertion sort for the initial ranges. - var windowSize = 4; - for (var start = 0; start < len - 1; start += windowSize) { - var end = std_Math_min(start + windowSize - 1, len - 1); - InsertionSort(lBuffer, start, end, comparefn); - } - - for (; windowSize < len; windowSize = 2 * windowSize) { - for (var start = 0; start < len; start += 2 * windowSize) { - // The midpoint between the two subarrays. - var mid = start + windowSize - 1; - - // To keep from going over the edge. - var end = std_Math_min(start + 2 * windowSize - 1, len - 1); - - MergeTypedArray(lBuffer, rBuffer, start, mid, end, comparefn); - } - - // Swap both lists. - var swap = lBuffer; - lBuffer = rBuffer; - rBuffer = swap; - } - - return lBuffer; -} diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index da7efd2fcc..10115b8699 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -2121,11 +2121,16 @@ static bool WasmDumpIon(JSContext* cx, unsigned argc, Value* vp) { return false; } - args.rval().set(StringValue(out.release(cx))); + JSString* str = out.release(cx); + if (!str) { + ReportOutOfMemory(cx); + return false; + } + args.rval().set(StringValue(str)); return true; } -enum class Flag { Tier2Complete, Deserialized }; +enum class Flag { Tier2Complete, Deserialized, ParsedBranchHints }; static bool WasmReturnFlag(JSContext* cx, unsigned argc, Value* vp, Flag flag) { CallArgs args = CallArgsFromVp(argc, vp); @@ -2150,6 +2155,9 @@ static bool WasmReturnFlag(JSContext* cx, unsigned argc, Value* vp, Flag flag) { case Flag::Deserialized: b = module->module().loggingDeserialized(); break; + case Flag::ParsedBranchHints: + b = module->module().metadata().parsedBranchHints; + break; } args.rval().set(BooleanValue(b)); @@ -2221,6 +2229,12 @@ static bool WasmLoadedFromCache(JSContext* cx, unsigned argc, Value* vp) { return WasmReturnFlag(cx, argc, vp, Flag::Deserialized); } +#ifdef ENABLE_WASM_BRANCH_HINTING +static bool WasmParsedBranchHints(JSContext* cx, unsigned argc, Value* vp) { + return WasmReturnFlag(cx, argc, vp, Flag::ParsedBranchHints); +} +#endif // ENABLE_WASM_BRANCH_HINTING + static bool WasmBuiltinI8VecMul(JSContext* cx, unsigned argc, Value* vp) { if (!wasm::HasSupport(cx)) { JS_ReportErrorASCII(cx, "wasm support unavailable"); @@ -3773,10 +3787,14 @@ static bool NewDependentString(JSContext* cx, unsigned argc, Value* vp) { } if (requiredHeap.isSome()) { - MOZ_ASSERT_IF(*requiredHeap == gc::Heap::Tenured, result->isTenured()); - if ((*requiredHeap == gc::Heap::Default) && result->isTenured()) { - JS_ReportErrorASCII(cx, "nursery string created in tenured heap"); - return false; + if ((*requiredHeap == gc::Heap::Tenured) != result->isTenured()) { + if (result->isTenured()) { + JS_ReportErrorASCII(cx, "nursery string created in tenured heap"); + return false; + } else { + JS_ReportErrorASCII(cx, "tenured string created in nursery heap"); + return false; + } } } @@ -6246,7 +6264,7 @@ void ShapeSnapshot::check(JSContext* cx, const ShapeSnapshot& later) const { if (object_->is()) { NativeObject* nobj = &object_->as(); if (nobj->inDictionaryMode()) { - MOZ_RELEASE_ASSERT(shape_ != later.shape_); + MOZ_RELEASE_ASSERT(nobj->shape() != later.shape_); } } return; @@ -9956,6 +9974,14 @@ JS_FOR_WASM_FEATURES(WASM_FEATURE) " Gets the length of a WebAssembly GC array."), #endif // ENABLE_WASM_GC +#ifdef ENABLE_WASM_BRANCH_HINTING + JS_FN_HELP("wasmParsedBranchHints", WasmParsedBranchHints, 1, 0, +"wasmParsedBranchHints(module)", +" Returns a boolean indicating whether a given module has successfully parsed a\n" +" custom branch hinting section."), + +#endif // ENABLE_WASM_BRANCH_HINTING + JS_FN_HELP("largeArrayBufferSupported", LargeArrayBufferSupported, 0, 0, "largeArrayBufferSupported()", " Returns true if array buffers larger than 2GB can be allocated."), diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js index 4d5474f6a7..46b81ebedc 100644 --- a/js/src/builtin/TypedArray.js +++ b/js/src/builtin/TypedArray.js @@ -1129,67 +1129,6 @@ function TypedArraySome(callbackfn /*, thisArg*/) { // Inlining this enables inlining of the callback function. SetIsInlinableLargeFunction(TypedArraySome); -// To satisfy step 6.b from TypedArray SortCompare described in 23.2.3.29 the -// user supplied comparefn is wrapped. -function TypedArraySortCompare(comparefn) { - return function(x, y) { - // Step 6.b.i. - var v = +callContentFunction(comparefn, undefined, x, y); - - // Step 6.b.ii. - if (v !== v) { - return 0; - } - - // Step 6.b.iii. - return v; - }; -} - -// ES2019 draft rev 8a16cb8d18660a1106faae693f0f39b9f1a30748 -// 22.2.3.26 %TypedArray%.prototype.sort ( comparefn ) -function TypedArraySort(comparefn) { - // This function is not generic. - - // Step 1. - if (comparefn !== undefined) { - if (!IsCallable(comparefn)) { - ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn)); - } - } - - // Step 2. - var obj = this; - - // Step 3. - EnsureTypedArrayWithArrayBuffer(obj); - - // Step 4. - var len = PossiblyWrappedTypedArrayLength(obj); - - // Arrays with less than two elements remain unchanged when sorted. - if (len <= 1) { - return obj; - } - - if (comparefn === undefined) { - return TypedArrayNativeSort(obj); - } - - // Steps 5-6. - var wrappedCompareFn = TypedArraySortCompare(comparefn); - - // Step 7. - var sorted = MergeSortTypedArray(obj, len, wrappedCompareFn); - - // Move the sorted elements into the array. - for (var i = 0; i < len; i++) { - obj[i] = sorted[i]; - } - - return obj; -} - // ES2017 draft rev f8a9be8ea4bd97237d176907a1e3080dce20c68f // 22.2.3.28 %TypedArray%.prototype.toLocaleString ([ reserved1 [ , reserved2 ] ]) // ES2017 Intl draft rev 78bbe7d1095f5ff3760ac4017ed366026e4cb276 @@ -2129,10 +2068,11 @@ function TypedArrayWith(index, value) { return A; } -// https://github.com/tc39/proposal-change-array-by-copy -// TypedArray.prototype.toSorted() +// https://tc39.es/ecma262/#sec-%typedarray%.prototype.tosorted +// 23.2.3.33 %TypedArray%.prototype.toSorted ( comparefn ) function TypedArrayToSorted(comparefn) { - // Step 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. + // Step 1. If comparefn is not undefined and IsCallable(comparefn) is false, + // throw a TypeError exception. if (comparefn !== undefined) { if (!IsCallable(comparefn)) { ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn)); @@ -2145,48 +2085,27 @@ function TypedArrayToSorted(comparefn) { // Step 3. Perform ? ValidateTypedArray(this). EnsureTypedArrayWithArrayBuffer(O); - // Step 4. omitted. Let buffer be obj.[[ViewedArrayBuffer]]. - // FIXME: Draft spec not synched with https://github.com/tc39/ecma262/pull/2723 - - // Step 5. Let len be O.[[ArrayLength]]. + // Step 4. Let len be TypedArrayLength(taRecord). var len = PossiblyWrappedTypedArrayLength(O); - // Arrays with less than two elements remain unchanged when sorted. - if (len <= 1) { - // Step 6. Let A be ? TypedArrayCreateSameType(O, « 𝔽(len) »). - var A = TypedArrayCreateSameType(O, len); + // Step 5. Let A be ? TypedArrayCreateSameType(O, « 𝔽(len) »). + var A = TypedArrayCreateSameType(O, len); - // Steps 7-11. - if (len > 0) { - A[0] = O[0]; - } + // Steps 6-10 not followed exactly; this implementation copies the list and then + // sorts the copy, rather than calling a sort method that copies the list and then + // copying the result again. - // Step 12. - return A; + // Equivalent to steps 9-10. + for (var k = 0; k < len; k++) { + A[k] = O[k]; } - if (comparefn === undefined) { - // Step 6. Let A be ? TypedArrayCreateSameType(O, « 𝔽(len) »). - var A = TypedArrayCreateSameType(O, len); - - // Steps 7-11 not followed exactly; this implementation copies the list and then - // sorts the copy, rather than calling a sort method that copies the list and then - // copying the result again. - - // Equivalent to steps 10-11. - for (var k = 0; k < len; k++) { - A[k] = O[k]; - } - - // Equivalent to steps 7-9 and 12. - return TypedArrayNativeSort(A); + // Arrays with less than two elements remain unchanged when sorted. + if (len > 1) { + // Equivalent to steps 6-8. + callFunction(std_TypedArray_sort, A, comparefn); } - // Steps 7-8. - var wrappedCompareFn = TypedArraySortCompare(comparefn); - - // Steps 6 and 9-12. - // - // MergeSortTypedArray returns a sorted copy - exactly what we need to return. - return MergeSortTypedArray(O, len, wrappedCompareFn); + // Step 11. + return A; } diff --git a/js/src/builtin/temporal/Calendar.cpp b/js/src/builtin/temporal/Calendar.cpp index 8a06877093..573a1740d2 100644 --- a/js/src/builtin/temporal/Calendar.cpp +++ b/js/src/builtin/temporal/Calendar.cpp @@ -137,18 +137,22 @@ static bool IterableToListOfStrings(JSContext* cx, Handle items, // Step 1. (Not applicable in our implementation.) - // Steps 2-3. + // Step 2. Rooted nextValue(cx); Rooted value(cx); while (true) { + // Step 2.a. bool done; if (!iterator.next(&nextValue, &done)) { return false; } + + // Step 2.b. if (done) { - break; + return true; } + // Step 2.d. (Reordered) if (nextValue.isString()) { if (!PrimitiveValueToId(cx, nextValue, &value)) { return false; @@ -159,15 +163,14 @@ static bool IterableToListOfStrings(JSContext* cx, Handle items, continue; } + // Step 2.c.1. ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, nullptr, "not a string"); + // Step 2.c.2. iterator.closeThrow(); return false; } - - // Step 4. - return true; } /** @@ -312,7 +315,7 @@ int32_t js::temporal::ToISODayOfYear(const PlainDate& date) { MOZ_ASSERT(ISODateTimeWithinLimits(date)); // Steps 1-5. - auto& [year, month, day] = date; + const auto& [year, month, day] = date; return ::ToISODayOfYear(year, month, day); } @@ -423,7 +426,7 @@ struct YearWeek final { static YearWeek ToISOWeekOfYear(const PlainDate& date) { MOZ_ASSERT(ISODateTimeWithinLimits(date)); - auto& [year, month, day] = date; + const auto& [year, month, day] = date; // TODO: https://en.wikipedia.org/wiki/Week#The_ISO_week_date_system // TODO: https://en.wikipedia.org/wiki/ISO_week_date#Algorithms @@ -1054,6 +1057,88 @@ static bool BuiltinCalendarFields( return true; } +/** + * Temporal.Calendar.prototype.fields ( fields ) + */ +static bool BuiltinCalendarFields(JSContext* cx, Handle fields, + MutableHandle result) { + // Step 4. + JS::ForOfIterator iterator(cx); + if (!iterator.init(fields)) { + return false; + } + + // Step 5. + JS::RootedVector fieldNames(cx); + mozilla::EnumSet seen; + + // Step 6. + Rooted nextValue(cx); + Rooted linear(cx); + while (true) { + // Step 6.a. + bool done; + if (!iterator.next(&nextValue, &done)) { + return false; + } + + // Step 6.b. + if (done) { + auto* array = + NewDenseCopiedArray(cx, fieldNames.length(), fieldNames.begin()); + if (!array) { + return false; + } + + result.setObject(*array); + return true; + } + + // Step 6.c. + if (!nextValue.isString()) { + // Step 6.c.1. + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, + nullptr, "not a string"); + + // Step 6.c.2. + iterator.closeThrow(); + return false; + } + + linear = nextValue.toString()->ensureLinear(cx); + if (!linear) { + return false; + } + + // Step 6.e. (Reordered) + CalendarField field; + if (!ToCalendarField(cx, linear, &field)) { + iterator.closeThrow(); + return false; + } + + // Step 6.d. + if (seen.contains(field)) { + // Step 6.d.1. + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_DUPLICATE_FIELD, + chars.get()); + } + + // Step 6.d.2. + iterator.closeThrow(); + return false; + } + + // Step 6.f. + if (!fieldNames.append(nextValue)) { + return false; + } + seen += field; + } +} + #ifdef DEBUG static bool IsSorted(std::initializer_list fieldNames) { return std::is_sorted(fieldNames.begin(), fieldNames.end(), @@ -1075,15 +1160,12 @@ bool js::temporal::CalendarFields( MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::Fields)); - // FIXME: spec issue - the input is already sorted, let's assert this, too. + // Step 1. MOZ_ASSERT(IsSorted(fieldNames)); - - // FIXME: spec issue - the input shouldn't have duplicate elements. Let's - // assert this, too. MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == fieldNames.end()); - // Step 1. + // Step 2. auto fields = calendar.fields(); if (!fields) { bool arrayIterationSane; @@ -1098,16 +1180,14 @@ bool js::temporal::CalendarFields( } if (arrayIterationSane) { - // Steps 1.a-b. (Not applicable in our implementation.) - - // Step 1.c. + // Steps 2.a-b. return BuiltinCalendarFields(cx, fieldNames, result.get()); - // Steps 1.d-f. (Not applicable in our implementation.) + // Steps 2.c-e. (Not applicable in our implementation.) } } - // Step 2. (Inlined call to CalendarMethodsRecordCall.) + // Step 3. (Inlined call to CalendarMethodsRecordCall.) auto* array = NewDenseFullyAllocatedArray(cx, fieldNames.size()); if (!array) { @@ -1120,15 +1200,22 @@ bool js::temporal::CalendarFields( array->initDenseElement(i, StringValue(name)); } - Rooted fieldsFn(cx, ObjectValue(*fields)); - auto thisv = calendar.receiver().toValue(); Rooted fieldsArray(cx, ObjectValue(*array)); - if (!Call(cx, fieldsFn, thisv, fieldsArray, &fieldsArray)) { - return false; + Rooted calendarFieldNames(cx); + if (fields) { + Rooted fieldsFn(cx, ObjectValue(*fields)); + auto thisv = calendar.receiver().toValue(); + if (!Call(cx, fieldsFn, thisv, fieldsArray, &calendarFieldNames)) { + return false; + } + } else { + if (!BuiltinCalendarFields(cx, fieldsArray, &calendarFieldNames)) { + return false; + } } - // Steps 3-4. - if (!IterableToListOfStrings(cx, fieldsArray, result)) { + // Steps 4-5. + if (!IterableToListOfStrings(cx, calendarFieldNames, result)) { return false; } @@ -1184,6 +1271,26 @@ static bool RequireIntegralPositiveNumber(JSContext* cx, Handle value, return true; } +static bool RequireIntegralNumberOrUndefined(JSContext* cx, Handle value, + Handle name, + MutableHandle result) { + if (value.isUndefined()) { + result.setUndefined(); + return true; + } + return RequireIntegralNumber(cx, value, name, result); +} + +static bool RequireIntegralPositiveNumberOrUndefined( + JSContext* cx, Handle value, Handle name, + MutableHandle result) { + if (value.isUndefined()) { + result.setUndefined(); + return true; + } + return RequireIntegralPositiveNumber(cx, value, name, result); +} + static bool RequireString(JSContext* cx, Handle value, Handle name, MutableHandle result) { @@ -1756,7 +1863,7 @@ static bool CalendarWeekOfYear(JSContext* cx, Handle calendar, MutableHandle result) { // Steps 1-6. return CallCalendarMethod( + RequireIntegralPositiveNumberOrUndefined>( cx, cx->names().weekOfYear, Calendar_weekOfYear, calendar, dateLike, date, result); } @@ -1821,7 +1928,8 @@ static bool CalendarYearOfWeek(JSContext* cx, Handle calendar, const PlainDate& date, MutableHandle result) { // Steps 1-5. - return CallCalendarMethod( + return CallCalendarMethod( cx, cx->names().yearOfWeek, Calendar_yearOfWeek, calendar, dateLike, date, result); } @@ -2482,8 +2590,8 @@ Wrapped js::temporal::CalendarDateFromFields( } struct RegulatedISOYearMonth final { - double year; - int32_t month; + double year = 0; + int32_t month = 0; }; /** @@ -2492,33 +2600,30 @@ struct RegulatedISOYearMonth final { static bool RegulateISOYearMonth(JSContext* cx, double year, double month, TemporalOverflow overflow, RegulatedISOYearMonth* result) { - // Step 1. MOZ_ASSERT(IsInteger(year)); MOZ_ASSERT(IsInteger(month)); - // Step 2. (Not applicable in our implementation.) - - // Step 3. + // Step 1. if (overflow == TemporalOverflow::Constrain) { - // Step 3.a. + // Step 1.a. month = std::clamp(month, 1.0, 12.0); - // Step 3.b. + // Step 1.b. *result = {year, int32_t(month)}; return true; } - // Step 4.a. + // Step 2.a. MOZ_ASSERT(overflow == TemporalOverflow::Reject); - // Step 4.b. + // Step 2.b. if (month < 1 || month > 12) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID); return false; } - // Step 4.c. + // Step 2.c. *result = {year, int32_t(month)}; return true; } @@ -2850,7 +2955,7 @@ static bool ISOFieldKeysToIgnore(JSContext* cx, const PropertyVector& keys, // Step 2. bool seenMonthOrMonthCode = false; - for (auto& key : keys) { + for (const auto& key : keys) { // Reorder the substeps in order to use |putNew| instead of |put|, because // the former is slightly faster. @@ -2874,7 +2979,7 @@ static bool ISOFieldKeysToIgnore(JSContext* cx, const PropertyVector& keys, } } - // Step 3. + // Steps 3-4. return true; } @@ -3060,7 +3165,7 @@ JSObject* js::temporal::CalendarMergeFields( * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) */ static bool BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, - const Duration& duration, + const NormalizedDuration& duration, Handle options, PlainDate* result) { MOZ_ASSERT(IsValidISODate(date)); MOZ_ASSERT(IsValidDuration(duration)); @@ -3076,17 +3181,53 @@ static bool BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, } // Step 8. - TimeDuration balanceResult; - if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &balanceResult)) { - return false; - } + const auto& timeDuration = duration.time; // Step 9. - Duration addDuration = {duration.years, duration.months, duration.weeks, - balanceResult.days}; + auto balanceResult = BalanceTimeDuration(timeDuration, TemporalUnit::Day); + + // Step 10. + auto addDuration = DateDuration{ + duration.date.years, + duration.date.months, + duration.date.weeks, + duration.date.days + balanceResult.days, + }; return AddISODate(cx, date, addDuration, overflow, result); } +/** + * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) + */ +static bool BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, + const DateDuration& duration, + Handle options, PlainDate* result) { + // Steps 1-6. (Not applicable) + + // Step 8. (Reordered) + auto normalized = CreateNormalizedDurationRecord(duration, {}); + + // Steps 7-10. + return BuiltinCalendarAdd(cx, date, normalized, options, result); +} + +/** + * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) + */ +static PlainDateObject* BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, + const DateDuration& duration, + Handle options) { + // Steps 1-10. + PlainDate result; + if (!BuiltinCalendarAdd(cx, date, duration, options, &result)) { + return nullptr; + } + + // Step 11. + Rooted calendar(cx, CalendarValue(cx->names().iso8601)); + return CreateTemporalDate(cx, result, calendar); +} + /** * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) */ @@ -3095,13 +3236,16 @@ static PlainDateObject* BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, Handle options) { // Steps 1-6. (Not applicable) - // Steps 7-9. + // Step 8. (Reordered) + auto normalized = CreateNormalizedDurationRecord(duration); + + // Steps 7-10. PlainDate result; - if (!BuiltinCalendarAdd(cx, date, duration, options, &result)) { + if (!BuiltinCalendarAdd(cx, date, normalized, options, &result)) { return nullptr; } - // Step 10. + // Step 11. Rooted calendar(cx, CalendarValue(cx->names().iso8601)); return CreateTemporalDate(cx, result, calendar); } @@ -3183,6 +3327,38 @@ static Wrapped CalendarDateAdd( return CalendarDateAddSlow(cx, calendar, date, durationObj, options); } +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +static Wrapped CalendarDateAdd( + JSContext* cx, Handle calendar, + Handle> date, const DateDuration& duration, + Handle options) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 1. (Not applicable). + + // Step 3. (Reordered) + if (!calendar.dateAdd()) { + auto* unwrappedDate = date.unwrap(cx); + if (!unwrappedDate) { + return nullptr; + } + auto date = ToPlainDate(unwrappedDate); + + return BuiltinCalendarAdd(cx, date, duration, options); + } + + // Steps 2 and 4-5. + Rooted durationObj( + cx, CreateTemporalDuration(cx, duration.toDuration())); + if (!durationObj) { + return nullptr; + } + return CalendarDateAddSlow(cx, calendar, date, durationObj, options); +} + /** * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) */ @@ -3221,8 +3397,8 @@ static Wrapped CalendarDateAdd( */ static bool CalendarDateAdd(JSContext* cx, Handle calendar, Handle> date, - const Duration& duration, Handle options, - PlainDate* result) { + const DateDuration& duration, + Handle options, PlainDate* result) { MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); @@ -3241,7 +3417,8 @@ static bool CalendarDateAdd(JSContext* cx, Handle calendar, // Steps 2 and 4-5. - Rooted durationObj(cx, CreateTemporalDuration(cx, duration)); + Rooted durationObj( + cx, CreateTemporalDuration(cx, duration.toDuration())); if (!durationObj) { return false; } @@ -3259,7 +3436,7 @@ static bool CalendarDateAdd(JSContext* cx, Handle calendar, * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) */ static bool CalendarDateAdd(JSContext* cx, Handle calendar, - const PlainDate& date, const Duration& duration, + const PlainDate& date, const DateDuration& duration, Handle options, PlainDate* result) { MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); @@ -3279,7 +3456,8 @@ static bool CalendarDateAdd(JSContext* cx, Handle calendar, return false; } - Rooted durationObj(cx, CreateTemporalDuration(cx, duration)); + Rooted durationObj( + cx, CreateTemporalDuration(cx, duration.toDuration())); if (!durationObj) { return false; } @@ -3311,7 +3489,7 @@ Wrapped js::temporal::CalendarDateAdd( */ Wrapped js::temporal::CalendarDateAdd( JSContext* cx, Handle calendar, - Handle> date, const Duration& duration) { + Handle> date, const DateDuration& duration) { // Step 1. Handle options = nullptr; @@ -3352,7 +3530,7 @@ Wrapped js::temporal::CalendarDateAdd( bool js::temporal::CalendarDateAdd(JSContext* cx, Handle calendar, const PlainDate& date, - const Duration& duration, + const DateDuration& duration, PlainDate* result) { // Step 1. Handle options = nullptr; @@ -3364,9 +3542,12 @@ bool js::temporal::CalendarDateAdd(JSContext* cx, /** * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) */ -bool js::temporal::CalendarDateAdd( - JSContext* cx, Handle calendar, const PlainDate& date, - const Duration& duration, Handle options, PlainDate* result) { +bool js::temporal::CalendarDateAdd(JSContext* cx, + Handle calendar, + const PlainDate& date, + const DateDuration& duration, + Handle options, + PlainDate* result) { // Step 1. (Not applicable) // Steps 2-5. @@ -3379,7 +3560,7 @@ bool js::temporal::CalendarDateAdd( bool js::temporal::CalendarDateAdd(JSContext* cx, Handle calendar, Handle> date, - const Duration& duration, + const DateDuration& duration, PlainDate* result) { // Step 1. Handle options = nullptr; @@ -3391,18 +3572,15 @@ bool js::temporal::CalendarDateAdd(JSContext* cx, /** * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) */ -static Duration BuiltinCalendarDateUntil(const PlainDate& one, - const PlainDate& two, - TemporalUnit largestUnit) { +static DateDuration BuiltinCalendarDateUntil(const PlainDate& one, + const PlainDate& two, + TemporalUnit largestUnit) { // Steps 1-3. (Not applicable) // Steps 4-8. (Not applicable) - // Step 9. - auto difference = DifferenceISODate(one, two, largestUnit); - - // Step 10. - return difference.toDuration(); + // Steps 9-10. + return DifferenceISODate(one, two, largestUnit); } /** @@ -3412,7 +3590,7 @@ static bool BuiltinCalendarDateUntil(JSContext* cx, Handle> one, Handle> two, TemporalUnit largestUnit, - Duration* result) { + DateDuration* result) { MOZ_ASSERT(largestUnit <= TemporalUnit::Day); auto* unwrappedOne = one.unwrap(cx); @@ -3432,37 +3610,31 @@ static bool BuiltinCalendarDateUntil(JSContext* cx, return true; } -/** - * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) - */ -static bool BuiltinCalendarDateUntil(JSContext* cx, - Handle> one, - Handle> two, - Handle options, - Duration* result) { - // Steps 1-6. (Not applicable) - - // Steps 7-8. - auto largestUnit = TemporalUnit::Day; - if (!GetTemporalUnit(cx, options, TemporalUnitKey::LargestUnit, - TemporalUnitGroup::Date, &largestUnit)) { - return false; - } - - // Steps 9-10. - return BuiltinCalendarDateUntil(cx, one, two, largestUnit, result); -} - static bool CalendarDateUntilSlow(JSContext* cx, Handle calendar, Handle> one, Handle> two, - Handle options, Duration* result) { + TemporalUnit largestUnit, + Handle maybeOptions, + DateDuration* result) { MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); MOZ_ASSERT(calendar.receiver().isObject()); MOZ_ASSERT(calendar.dateUntil()); + Rooted options(cx, maybeOptions); + if (!options) { + options = NewPlainObjectWithProto(cx, nullptr); + if (!options) { + return false; + } + } + + Rooted value(cx, StringValue(TemporalUnitToString(cx, largestUnit))); + if (!DefineDataProperty(cx, options, cx->names().largestUnit, value)) { + return false; + } + // Step 1. (Inlined call to CalendarMethodsRecordCall.) Rooted dateUntil(cx, ObjectValue(*calendar.dateUntil())); auto thisv = calendar.receiver().toValue(); @@ -3488,29 +3660,90 @@ static bool CalendarDateUntilSlow(JSContext* cx, } // Step 4. - *result = ToDuration(&rval.toObject().unwrapAs()); + auto duration = ToDuration(&rval.toObject().unwrapAs()); + *result = duration.toDateDuration(); return true; } +static bool CalendarDateUntilSlow(JSContext* cx, + Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, + Handle maybeOptions, + DateDuration* result) { + Rooted date1( + cx, CreateTemporalDate(cx, one, calendar.receiver())); + if (!date1) { + return false; + } + + Rooted date2( + cx, CreateTemporalDate(cx, two, calendar.receiver())); + if (!date2) { + return false; + } + + return CalendarDateUntilSlow(cx, calendar, date1, date2, largestUnit, + maybeOptions, result); +} + /** * CalendarDateUntil ( calendarRec, one, two, options ) */ bool js::temporal::CalendarDateUntil(JSContext* cx, Handle calendar, - Handle> one, - Handle> two, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, + DateDuration* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + MOZ_ASSERT(largestUnit <= TemporalUnit::Day); + + // Step 2. (Reordered) + if (!calendar.dateUntil()) { + *result = BuiltinCalendarDateUntil(one, two, largestUnit); + return true; + } + + // Steps 1 and 3-4. + return CalendarDateUntilSlow(cx, calendar, one, two, largestUnit, nullptr, + result); +} + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool js::temporal::CalendarDateUntil(JSContext* cx, + Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, Handle options, - Duration* result) { + DateDuration* result) { MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + // As an optimization, our implementation only adds |largestUnit| to the + // options object when taking the slow-path. +#ifdef DEBUG + // The object must be extensible, otherwise we'd need to throw an error when + // attempting to add the "largestUnit" property to a non-extensible object. + MOZ_ASSERT(options->isExtensible()); + + // Similarily, if there's an existing "largestUnit" property, this property + // must be configurable. + auto largestUnitProp = options->lookupPure(cx->names().largestUnit); + MOZ_ASSERT_IF(largestUnitProp, largestUnitProp->configurable()); +#endif + // Step 2. (Reordered) if (!calendar.dateUntil()) { - return BuiltinCalendarDateUntil(cx, one, two, options, result); + *result = BuiltinCalendarDateUntil(one, two, largestUnit); + return true; } // Steps 1 and 3-4. - return CalendarDateUntilSlow(cx, calendar, one, two, options, result); + return CalendarDateUntilSlow(cx, calendar, one, two, largestUnit, options, + result); } /** @@ -3521,7 +3754,7 @@ bool js::temporal::CalendarDateUntil(JSContext* cx, Handle> one, Handle> two, TemporalUnit largestUnit, - Duration* result) { + DateDuration* result) { MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); MOZ_ASSERT(largestUnit <= TemporalUnit::Day); @@ -3531,18 +3764,45 @@ bool js::temporal::CalendarDateUntil(JSContext* cx, return BuiltinCalendarDateUntil(cx, one, two, largestUnit, result); } - Rooted untilOptions(cx, NewPlainObjectWithProto(cx, nullptr)); - if (!untilOptions) { - return false; - } + // Steps 1 and 3-4. + return CalendarDateUntilSlow(cx, calendar, one, two, largestUnit, nullptr, + result); +} - Rooted value(cx, StringValue(TemporalUnitToString(cx, largestUnit))); - if (!DefineDataProperty(cx, untilOptions, cx->names().largestUnit, value)) { - return false; +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool js::temporal::CalendarDateUntil(JSContext* cx, + Handle calendar, + Handle> one, + Handle> two, + TemporalUnit largestUnit, + Handle options, + DateDuration* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + + // As an optimization, our implementation only adds |largestUnit| to the + // options object when taking the slow-path. +#ifdef DEBUG + // The object must be extensible, otherwise we'd need to throw an error when + // attempting to add the "largestUnit" property to a non-extensible object. + MOZ_ASSERT(options->isExtensible()); + + // Similarily, if there's an existing "largestUnit" property, this property + // must be configurable. + auto largestUnitProp = options->lookupPure(cx->names().largestUnit); + MOZ_ASSERT_IF(largestUnitProp, largestUnitProp->configurable()); +#endif + + // Step 2. (Reordered) + if (!calendar.dateUntil()) { + return BuiltinCalendarDateUntil(cx, one, two, largestUnit, result); } // Steps 1 and 3-4. - return CalendarDateUntilSlow(cx, calendar, one, two, untilOptions, result); + return CalendarDateUntilSlow(cx, calendar, one, two, largestUnit, options, + result); } /** @@ -3925,7 +4185,7 @@ static bool Calendar_dateAdd(JSContext* cx, const CallArgs& args) { } } - // Steps 7-10. + // Steps 7-11. auto* obj = BuiltinCalendarAdd(cx, date, duration, options); if (!obj) { return false; @@ -3979,10 +4239,11 @@ static bool Calendar_dateUntil(JSContext* cx, const CallArgs& args) { } } - // Steps 9-10. + // Step 9. auto duration = BuiltinCalendarDateUntil(one, two, largestUnit); - auto* obj = CreateTemporalDuration(cx, duration); + // Step 10. + auto* obj = CreateTemporalDuration(cx, duration.toDuration()); if (!obj) { return false; } @@ -4363,76 +4624,8 @@ static bool Calendar_fields(JSContext* cx, const CallArgs& args) { // Step 3. MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as())); - // Step 4. - JS::ForOfIterator iterator(cx); - if (!iterator.init(args.get(0))) { - return false; - } - - // Step 5. - JS::RootedVector fieldNames(cx); - mozilla::EnumSet seen; - - // Steps 6-7. - Rooted nextValue(cx); - Rooted linear(cx); - while (true) { - // Steps 7.a and 7.b.i. - bool done; - if (!iterator.next(&nextValue, &done)) { - return false; - } - if (done) { - break; - } - - // Step 7.b.ii. - if (!nextValue.isString()) { - ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, - nullptr, "not a string"); - iterator.closeThrow(); - return false; - } - - linear = nextValue.toString()->ensureLinear(cx); - if (!linear) { - return false; - } - - // Step 7.b.iv. (Reordered) - CalendarField field; - if (!ToCalendarField(cx, linear, &field)) { - iterator.closeThrow(); - return false; - } - - // Step 7.b.iii. - if (seen.contains(field)) { - if (auto chars = QuoteString(cx, linear, '"')) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_CALENDAR_DUPLICATE_FIELD, - chars.get()); - } - iterator.closeThrow(); - return false; - } - - // Step 7.b.v. - if (!fieldNames.append(nextValue)) { - return false; - } - seen += field; - } - - // Step 8. - auto* array = - NewDenseCopiedArray(cx, fieldNames.length(), fieldNames.begin()); - if (!array) { - return false; - } - - args.rval().setObject(*array); - return true; + // Steps 4-6. + return BuiltinCalendarFields(cx, args.get(0), args.rval()); } /** @@ -4512,18 +4705,14 @@ static bool Calendar_mergeFields(JSContext* cx, const CallArgs& args) { for (size_t i = 0; i < fieldsKeys.length(); i++) { Handle key = fieldsKeys[i]; - // Step 12.a. - // FIXME: spec issue - unnecessary initialisation - // https://github.com/tc39/proposal-temporal/issues/2549 - - // Steps 12.b-c. + // Steps 12.a-b. if (overriddenKeys.has(key)) { if (!GetProperty(cx, additionalFieldsCopy, additionalFieldsCopy, key, &propValue)) { return false; } - // Step 12.d. (Reordered) + // Step 12.c. (Reordered) if (propValue.isUndefined()) { // The property can be undefined if the key is "month" or "monthCode". MOZ_ASSERT(key.isAtom(cx->names().month) || @@ -4540,7 +4729,7 @@ static bool Calendar_mergeFields(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(!propValue.isUndefined()); } - // Step 12.d. + // Step 12.c. if (!DefineDataProperty(cx, merged, key, propValue)) { return false; } diff --git a/js/src/builtin/temporal/Calendar.h b/js/src/builtin/temporal/Calendar.h index f80f528d83..dd06c11923 100644 --- a/js/src/builtin/temporal/Calendar.h +++ b/js/src/builtin/temporal/Calendar.h @@ -8,6 +8,7 @@ #define builtin_temporal_Calendar_h #include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" #include "mozilla/EnumSet.h" #include @@ -49,7 +50,7 @@ class CalendarObject : public NativeObject { * Calendar value, which is either a string containing a canonical calendar * identifier or an object. */ -class CalendarValue final { +class MOZ_STACK_CLASS CalendarValue final { JS::Value value_{}; public: @@ -126,7 +127,7 @@ enum class CalendarMethod { YearMonthFromFields, }; -class CalendarRecord { +class MOZ_STACK_CLASS CalendarRecord final { CalendarValue receiver_; // Null unless non-builtin calendar methods are used. @@ -190,6 +191,7 @@ class CalendarRecord { void trace(JSTracer* trc); }; +struct DateDuration; struct Duration; struct PlainDate; struct PlainDateTime; @@ -199,6 +201,7 @@ class PlainDateTimeObject; class PlainMonthDayObject; class PlainYearMonthObject; enum class CalendarOption; +enum class TemporalOverflow; enum class TemporalUnit; /** @@ -305,7 +308,7 @@ JSObject* CalendarMergeFields(JSContext* cx, */ Wrapped CalendarDateAdd( JSContext* cx, JS::Handle calendar, - JS::Handle> date, const Duration& duration); + JS::Handle> date, const DateDuration& duration); /** * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) @@ -336,14 +339,14 @@ Wrapped CalendarDateAdd( * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) */ bool CalendarDateAdd(JSContext* cx, JS::Handle calendar, - const PlainDate& date, const Duration& duration, + const PlainDate& date, const DateDuration& duration, PlainDate* result); /** * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) */ bool CalendarDateAdd(JSContext* cx, JS::Handle calendar, - const PlainDate& date, const Duration& duration, + const PlainDate& date, const DateDuration& duration, JS::Handle options, PlainDate* result); /** @@ -351,7 +354,22 @@ bool CalendarDateAdd(JSContext* cx, JS::Handle calendar, */ bool CalendarDateAdd(JSContext* cx, JS::Handle calendar, JS::Handle> date, - const Duration& duration, PlainDate* result); + const DateDuration& duration, PlainDate* result); + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool CalendarDateUntil(JSContext* cx, JS::Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, DateDuration* result); + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool CalendarDateUntil(JSContext* cx, JS::Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, + JS::Handle options, DateDuration* result); /** * CalendarDateUntil ( calendarRec, one, two, options ) @@ -359,7 +377,7 @@ bool CalendarDateAdd(JSContext* cx, JS::Handle calendar, bool CalendarDateUntil(JSContext* cx, JS::Handle calendar, JS::Handle> one, JS::Handle> two, - JS::Handle options, Duration* result); + TemporalUnit largestUnit, DateDuration* result); /** * CalendarDateUntil ( calendarRec, one, two, options ) @@ -367,7 +385,8 @@ bool CalendarDateUntil(JSContext* cx, JS::Handle calendar, bool CalendarDateUntil(JSContext* cx, JS::Handle calendar, JS::Handle> one, JS::Handle> two, - TemporalUnit largestUnit, Duration* result); + TemporalUnit largestUnit, + JS::Handle options, DateDuration* result); /** * CalendarYear ( calendar, dateLike ) diff --git a/js/src/builtin/temporal/Duration.cpp b/js/src/builtin/temporal/Duration.cpp index 7e922aa68b..8f336a9a14 100644 --- a/js/src/builtin/temporal/Duration.cpp +++ b/js/src/builtin/temporal/Duration.cpp @@ -7,6 +7,7 @@ #include "builtin/temporal/Duration.h" #include "mozilla/Assertions.h" +#include "mozilla/Casting.h" #include "mozilla/CheckedInt.h" #include "mozilla/EnumSet.h" #include "mozilla/FloatingPoint.h" @@ -26,6 +27,8 @@ #include "builtin/temporal/Calendar.h" #include "builtin/temporal/Instant.h" +#include "builtin/temporal/Int128.h" +#include "builtin/temporal/Int96.h" #include "builtin/temporal/PlainDate.h" #include "builtin/temporal/PlainDateTime.h" #include "builtin/temporal/Temporal.h" @@ -54,7 +57,6 @@ #include "js/RootingAPI.h" #include "js/Value.h" #include "util/StringBuffer.h" -#include "vm/BigIntType.h" #include "vm/BytecodeUtil.h" #include "vm/GlobalObject.h" #include "vm/JSAtomState.h" @@ -81,8 +83,8 @@ static bool IsIntegerOrInfinity(double d) { } static bool IsIntegerOrInfinityDuration(const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Integers exceeding the Number range are represented as infinity. @@ -94,8 +96,8 @@ static bool IsIntegerOrInfinityDuration(const Duration& duration) { } static bool IsIntegerDuration(const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; return IsInteger(years) && IsInteger(months) && IsInteger(weeks) && IsInteger(days) && IsInteger(hours) && IsInteger(minutes) && @@ -104,6 +106,12 @@ static bool IsIntegerDuration(const Duration& duration) { } #endif +static constexpr bool IsSafeInteger(int64_t x) { + constexpr int64_t MaxSafeInteger = int64_t(1) << 53; + constexpr int64_t MinSafeInteger = -MaxSafeInteger; + return MinSafeInteger < x && x < MaxSafeInteger; +} + /** * DurationSign ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) @@ -111,8 +119,8 @@ static bool IsIntegerDuration(const Duration& duration) { int32_t js::temporal::DurationSign(const Duration& duration) { MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Step 1. for (auto v : {years, months, weeks, days, hours, minutes, seconds, @@ -133,235 +141,201 @@ int32_t js::temporal::DurationSign(const Duration& duration) { } /** - * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * DurationSign ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) */ -bool js::temporal::IsValidDuration(const Duration& duration) { - MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; +int32_t js::temporal::DurationSign(const DateDuration& duration) { + const auto& [years, months, weeks, days] = duration; // Step 1. - int32_t sign = DurationSign(duration); - - // Step 2. - for (auto v : {years, months, weeks, days, hours, minutes, seconds, - milliseconds, microseconds, nanoseconds}) { - // Step 2.a. - if (!std::isfinite(v)) { - return false; - } - - // Step 2.b. - if (v < 0 && sign > 0) { - return false; + for (auto v : {years, months, weeks, days}) { + // Step 1.a. + if (v < 0) { + return -1; } - // Step 2.c. - if (v > 0 && sign < 0) { - return false; + // Step 1.b. + if (v > 0) { + return 1; } } - // Step 3. - return true; + // Step 2. + return 0; } /** - * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds ) + * Normalize a nanoseconds amount into a time duration. */ -bool js::temporal::ThrowIfInvalidDuration(JSContext* cx, - const Duration& duration) { - MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; +static NormalizedTimeDuration NormalizeNanoseconds(const Int96& nanoseconds) { + // Split into seconds and nanoseconds. + auto [seconds, nanos] = nanoseconds / ToNanoseconds(TemporalUnit::Second); - // Step 1. - int32_t sign = DurationSign(duration); + return {seconds, nanos}; +} - auto report = [&](double v, const char* name, unsigned errorNumber) { - ToCStringBuf cbuf; - const char* numStr = NumberToCString(&cbuf, v); +/** + * Normalize a nanoseconds amount into a time duration. Return Nothing if the + * value is too large. + */ +static mozilla::Maybe NormalizeNanoseconds( + double nanoseconds) { + MOZ_ASSERT(IsInteger(nanoseconds)); - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber, name, - numStr); - }; + if (auto int96 = Int96::fromInteger(nanoseconds)) { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = + Int96{uint64_t(1) << 53} * ToNanoseconds(TemporalUnit::Second); - auto throwIfInvalid = [&](double v, const char* name) { - // Step 2.a. - if (!std::isfinite(v)) { - report(v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); - return false; + if (int96->abs() < limit) { + return mozilla::Some(NormalizeNanoseconds(*int96)); } + } + return mozilla::Nothing(); +} - // Steps 2.b-c. - if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) { - report(v, name, JSMSG_TEMPORAL_DURATION_INVALID_SIGN); - return false; - } +/** + * Normalize a microseconds amount into a time duration. + */ +static NormalizedTimeDuration NormalizeMicroseconds(const Int96& microseconds) { + // Split into seconds and microseconds. + auto [seconds, micros] = microseconds / ToMicroseconds(TemporalUnit::Second); - return true; - }; + // Scale microseconds to nanoseconds. + int32_t nanos = micros * int32_t(ToNanoseconds(TemporalUnit::Microsecond)); - // Step 2. - if (!throwIfInvalid(years, "years")) { - return false; - } - if (!throwIfInvalid(months, "months")) { - return false; - } - if (!throwIfInvalid(weeks, "weeks")) { - return false; - } - if (!throwIfInvalid(days, "days")) { - return false; - } - if (!throwIfInvalid(hours, "hours")) { - return false; - } - if (!throwIfInvalid(minutes, "minutes")) { - return false; - } - if (!throwIfInvalid(seconds, "seconds")) { - return false; - } - if (!throwIfInvalid(milliseconds, "milliseconds")) { - return false; - } - if (!throwIfInvalid(microseconds, "microseconds")) { - return false; - } - if (!throwIfInvalid(nanoseconds, "nanoseconds")) { - return false; - } + return {seconds, nanos}; +} - MOZ_ASSERT(IsValidDuration(duration)); +/** + * Normalize a microseconds amount into a time duration. Return Nothing if the + * value is too large. + */ +static mozilla::Maybe NormalizeMicroseconds( + double microseconds) { + MOZ_ASSERT(IsInteger(microseconds)); - // Step 3. - return true; + if (auto int96 = Int96::fromInteger(microseconds)) { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = + Int96{uint64_t(1) << 53} * ToMicroseconds(TemporalUnit::Second); + + if (int96->abs() < limit) { + return mozilla::Some(NormalizeMicroseconds(*int96)); + } + } + return mozilla::Nothing(); } /** - * DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds ) + * Normalize a duration into a time duration. Return Nothing if any duration + * value is too large. */ -static TemporalUnit DefaultTemporalLargestUnit(const Duration& duration) { - MOZ_ASSERT(IsIntegerDuration(duration)); +static mozilla::Maybe NormalizeSeconds( + const Duration& duration) { + do { + auto nanoseconds = NormalizeNanoseconds(duration.nanoseconds); + if (!nanoseconds) { + break; + } + MOZ_ASSERT(IsValidNormalizedTimeDuration(*nanoseconds)); - // Step 1. - if (duration.years != 0) { - return TemporalUnit::Year; - } + auto microseconds = NormalizeMicroseconds(duration.microseconds); + if (!microseconds) { + break; + } + MOZ_ASSERT(IsValidNormalizedTimeDuration(*microseconds)); - // Step 2. - if (duration.months != 0) { - return TemporalUnit::Month; - } + // Overflows for millis/seconds/minutes/hours/days always result in an + // invalid normalized time duration. - // Step 3. - if (duration.weeks != 0) { - return TemporalUnit::Week; - } + int64_t milliseconds; + if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { + break; + } - // Step 4. - if (duration.days != 0) { - return TemporalUnit::Day; - } + int64_t seconds; + if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { + break; + } - // Step 5. - if (duration.hours != 0) { - return TemporalUnit::Hour; - } + int64_t minutes; + if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { + break; + } - // Step 6. - if (duration.minutes != 0) { - return TemporalUnit::Minute; - } + int64_t hours; + if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { + break; + } - // Step 7. - if (duration.seconds != 0) { - return TemporalUnit::Second; - } + int64_t days; + if (!mozilla::NumberEqualsInt64(duration.days, &days)) { + break; + } - // Step 8. - if (duration.milliseconds != 0) { - return TemporalUnit::Millisecond; - } + // Compute the overall amount of milliseconds. + mozilla::CheckedInt64 millis = days; + millis *= 24; + millis += hours; + millis *= 60; + millis += minutes; + millis *= 60; + millis += seconds; + millis *= 1000; + millis += milliseconds; + if (!millis.isValid()) { + break; + } - // Step 9. - if (duration.microseconds != 0) { - return TemporalUnit::Microsecond; - } + auto milli = NormalizedTimeDuration::fromMilliseconds(millis.value()); + if (!IsValidNormalizedTimeDuration(milli)) { + break; + } - // Step 10. - return TemporalUnit::Nanosecond; + // Compute the overall time duration. + auto result = milli + *microseconds + *nanoseconds; + if (!IsValidNormalizedTimeDuration(result)) { + break; + } + + return mozilla::Some(result); + } while (false); + + return mozilla::Nothing(); } /** - * CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds [ , newTarget ] ) + * Normalize a days amount into a time duration. Return Nothing if the value is + * too large. */ -static DurationObject* CreateTemporalDuration(JSContext* cx, - const CallArgs& args, - const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; - - // Step 1. - if (!ThrowIfInvalidDuration(cx, duration)) { - return nullptr; - } - - // Steps 2-3. - Rooted proto(cx); - if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Duration, &proto)) { - return nullptr; - } +static mozilla::Maybe NormalizeDays(int64_t days) { + do { + // Compute the overall amount of milliseconds. + auto millis = + mozilla::CheckedInt64(days) * ToMilliseconds(TemporalUnit::Day); + if (!millis.isValid()) { + break; + } - auto* object = NewObjectWithClassProto(cx, proto); - if (!object) { - return nullptr; - } + auto result = NormalizedTimeDuration::fromMilliseconds(millis.value()); + if (!IsValidNormalizedTimeDuration(result)) { + break; + } - // Steps 4-13. - // Add zero to convert -0 to +0. - object->setFixedSlot(DurationObject::YEARS_SLOT, NumberValue(years + (+0.0))); - object->setFixedSlot(DurationObject::MONTHS_SLOT, - NumberValue(months + (+0.0))); - object->setFixedSlot(DurationObject::WEEKS_SLOT, NumberValue(weeks + (+0.0))); - object->setFixedSlot(DurationObject::DAYS_SLOT, NumberValue(days + (+0.0))); - object->setFixedSlot(DurationObject::HOURS_SLOT, NumberValue(hours + (+0.0))); - object->setFixedSlot(DurationObject::MINUTES_SLOT, - NumberValue(minutes + (+0.0))); - object->setFixedSlot(DurationObject::SECONDS_SLOT, - NumberValue(seconds + (+0.0))); - object->setFixedSlot(DurationObject::MILLISECONDS_SLOT, - NumberValue(milliseconds + (+0.0))); - object->setFixedSlot(DurationObject::MICROSECONDS_SLOT, - NumberValue(microseconds + (+0.0))); - object->setFixedSlot(DurationObject::NANOSECONDS_SLOT, - NumberValue(nanoseconds + (+0.0))); + return mozilla::Some(result); + } while (false); - // Step 14. - return object; + return mozilla::Nothing(); } /** - * CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds [ , newTarget ] ) + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) */ -DurationObject* js::temporal::CreateTemporalDuration(JSContext* cx, - const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; - - MOZ_ASSERT(IsInteger(years)); - MOZ_ASSERT(IsInteger(months)); - MOZ_ASSERT(IsInteger(weeks)); - MOZ_ASSERT(IsInteger(days)); +static NormalizedTimeDuration NormalizeTimeDuration( + double hours, double minutes, double seconds, double milliseconds, + double microseconds, double nanoseconds) { MOZ_ASSERT(IsInteger(hours)); MOZ_ASSERT(IsInteger(minutes)); MOZ_ASSERT(IsInteger(seconds)); @@ -369,3996 +343,2988 @@ DurationObject* js::temporal::CreateTemporalDuration(JSContext* cx, MOZ_ASSERT(IsInteger(microseconds)); MOZ_ASSERT(IsInteger(nanoseconds)); - // Step 1. - if (!ThrowIfInvalidDuration(cx, duration)) { - return nullptr; - } + // Steps 1-3. + mozilla::CheckedInt64 millis = int64_t(hours); + millis *= 60; + millis += int64_t(minutes); + millis *= 60; + millis += int64_t(seconds); + millis *= 1000; + millis += int64_t(milliseconds); + MOZ_ASSERT(millis.isValid()); - // Steps 2-3. - auto* object = NewBuiltinClassInstance(cx); - if (!object) { - return nullptr; - } + auto normalized = NormalizedTimeDuration::fromMilliseconds(millis.value()); - // Steps 4-13. - // Add zero to convert -0 to +0. - object->setFixedSlot(DurationObject::YEARS_SLOT, NumberValue(years + (+0.0))); - object->setFixedSlot(DurationObject::MONTHS_SLOT, - NumberValue(months + (+0.0))); - object->setFixedSlot(DurationObject::WEEKS_SLOT, NumberValue(weeks + (+0.0))); - object->setFixedSlot(DurationObject::DAYS_SLOT, NumberValue(days + (+0.0))); - object->setFixedSlot(DurationObject::HOURS_SLOT, NumberValue(hours + (+0.0))); - object->setFixedSlot(DurationObject::MINUTES_SLOT, - NumberValue(minutes + (+0.0))); - object->setFixedSlot(DurationObject::SECONDS_SLOT, - NumberValue(seconds + (+0.0))); - object->setFixedSlot(DurationObject::MILLISECONDS_SLOT, - NumberValue(milliseconds + (+0.0))); - object->setFixedSlot(DurationObject::MICROSECONDS_SLOT, - NumberValue(microseconds + (+0.0))); - object->setFixedSlot(DurationObject::NANOSECONDS_SLOT, - NumberValue(nanoseconds + (+0.0))); + // Step 4. + auto micros = Int96::fromInteger(microseconds); + MOZ_ASSERT(micros); - // Step 14. - return object; + normalized += NormalizeMicroseconds(*micros); + + // Step 5. + auto nanos = Int96::fromInteger(nanoseconds); + MOZ_ASSERT(nanos); + + normalized += NormalizeNanoseconds(*nanos); + + // Step 6. + MOZ_ASSERT(IsValidNormalizedTimeDuration(normalized)); + + // Step 7. + return normalized; } /** - * ToIntegerIfIntegral ( argument ) + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) */ -static bool ToIntegerIfIntegral(JSContext* cx, const char* name, - Handle argument, double* num) { +NormalizedTimeDuration js::temporal::NormalizeTimeDuration( + int32_t hours, int32_t minutes, int32_t seconds, int32_t milliseconds, + int32_t microseconds, int32_t nanoseconds) { + // Steps 1-3. + mozilla::CheckedInt64 millis = int64_t(hours); + millis *= 60; + millis += int64_t(minutes); + millis *= 60; + millis += int64_t(seconds); + millis *= 1000; + millis += int64_t(milliseconds); + MOZ_ASSERT(millis.isValid()); + + auto normalized = NormalizedTimeDuration::fromMilliseconds(millis.value()); + + // Step 4. + normalized += NormalizeMicroseconds(Int96{microseconds}); + + // Step 5. + normalized += NormalizeNanoseconds(Int96{nanoseconds}); + + // Step 6. + MOZ_ASSERT(IsValidNormalizedTimeDuration(normalized)); + + // Step 7. + return normalized; +} + +/** + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) + */ +NormalizedTimeDuration js::temporal::NormalizeTimeDuration( + const Duration& duration) { + MOZ_ASSERT(IsValidDuration(duration)); + + return ::NormalizeTimeDuration(duration.hours, duration.minutes, + duration.seconds, duration.milliseconds, + duration.microseconds, duration.nanoseconds); +} + +/** + * AddNormalizedTimeDuration ( one, two ) + */ +static bool AddNormalizedTimeDuration(JSContext* cx, + const NormalizedTimeDuration& one, + const NormalizedTimeDuration& two, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(one)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(two)); + // Step 1. - double d; - if (!JS::ToNumber(cx, argument, &d)) { - return false; - } + auto sum = one + two; // Step 2. - if (!js::IsInteger(d)) { - ToCStringBuf cbuf; - const char* numStr = NumberToCString(&cbuf, d); - + if (!IsValidNormalizedTimeDuration(sum)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_NOT_INTEGER, numStr, - name); + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } // Step 3. - *num = d; + *result = sum; return true; } /** - * ToIntegerIfIntegral ( argument ) + * SubtractNormalizedTimeDuration ( one, two ) */ -static bool ToIntegerIfIntegral(JSContext* cx, Handle name, - Handle argument, double* result) { +static bool SubtractNormalizedTimeDuration(JSContext* cx, + const NormalizedTimeDuration& one, + const NormalizedTimeDuration& two, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(one)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(two)); + // Step 1. - double d; - if (!JS::ToNumber(cx, argument, &d)) { - return false; - } + auto sum = one - two; // Step 2. - if (!js::IsInteger(d)) { - if (auto nameStr = js::QuoteString(cx, name)) { - ToCStringBuf cbuf; - const char* numStr = NumberToCString(&cbuf, d); - - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_NOT_INTEGER, numStr, - nameStr.get()); - } + if (!IsValidNormalizedTimeDuration(sum)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } // Step 3. - *result = d; + *result = sum; return true; } /** - * ToTemporalPartialDurationRecord ( temporalDurationLike ) + * Add24HourDaysToNormalizedTimeDuration ( d, days ) */ -static bool ToTemporalPartialDurationRecord( - JSContext* cx, Handle temporalDurationLike, Duration* result) { - // Steps 1-3. (Not applicable in our implementation.) - - Rooted value(cx); - bool any = false; - - auto getDurationProperty = [&](Handle name, double* num) { - if (!GetProperty(cx, temporalDurationLike, temporalDurationLike, name, - &value)) { - return false; - } - - if (!value.isUndefined()) { - any = true; - - if (!ToIntegerIfIntegral(cx, name, value, num)) { - return false; - } - } - return true; - }; +bool js::temporal::Add24HourDaysToNormalizedTimeDuration( + JSContext* cx, const NormalizedTimeDuration& d, int64_t days, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(d)); - // Steps 4-23. - if (!getDurationProperty(cx->names().days, &result->days)) { - return false; - } - if (!getDurationProperty(cx->names().hours, &result->hours)) { - return false; - } - if (!getDurationProperty(cx->names().microseconds, &result->microseconds)) { - return false; - } - if (!getDurationProperty(cx->names().milliseconds, &result->milliseconds)) { - return false; - } - if (!getDurationProperty(cx->names().minutes, &result->minutes)) { - return false; - } - if (!getDurationProperty(cx->names().months, &result->months)) { - return false; - } - if (!getDurationProperty(cx->names().nanoseconds, &result->nanoseconds)) { - return false; - } - if (!getDurationProperty(cx->names().seconds, &result->seconds)) { - return false; - } - if (!getDurationProperty(cx->names().weeks, &result->weeks)) { - return false; - } - if (!getDurationProperty(cx->names().years, &result->years)) { + // Step 1. + auto normalizedDays = NormalizeDays(days); + if (!normalizedDays) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - // Step 24. - if (!any) { + // Step 2. + auto sum = d + *normalizedDays; + if (!IsValidNormalizedTimeDuration(sum)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_MISSING_UNIT); + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - // Step 25. + // Step 3. + *result = sum; return true; } /** - * ToTemporalDurationRecord ( temporalDurationLike ) + * CombineDateAndNormalizedTimeDuration ( dateDurationRecord, norm ) */ -bool js::temporal::ToTemporalDurationRecord(JSContext* cx, - Handle temporalDurationLike, - Duration* result) { - // Step 1. - if (!temporalDurationLike.isObject()) { - // Step 1.a. - if (!temporalDurationLike.isString()) { - ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, - temporalDurationLike, nullptr, "not a string"); - return false; - } - Rooted string(cx, temporalDurationLike.toString()); - - // Step 1.b. - return ParseTemporalDurationString(cx, string, result); - } +bool js::temporal::CombineDateAndNormalizedTimeDuration( + JSContext* cx, const DateDuration& date, const NormalizedTimeDuration& time, + NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(date)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(time)); - Rooted durationLike(cx, &temporalDurationLike.toObject()); + // Step 1. + int32_t dateSign = DurationSign(date); // Step 2. - if (auto* duration = durationLike->maybeUnwrapIf()) { - *result = ToDuration(duration); - return true; - } - - // Step 3. - Duration duration = {}; + int32_t timeSign = NormalizedTimeDurationSign(time); - // Steps 4-14. - if (!ToTemporalPartialDurationRecord(cx, durationLike, &duration)) { - return false; - } - - // Step 15. - if (!ThrowIfInvalidDuration(cx, duration)) { + // Step 3 + if ((dateSign * timeSign) < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_COMBINE_INVALID_SIGN); return false; } - // Step 16. - *result = duration; + // Step 4. + *result = {date, time}; return true; } /** - * ToTemporalDuration ( item ) + * NormalizedTimeDurationFromEpochNanosecondsDifference ( one, two ) */ -Wrapped js::temporal::ToTemporalDuration(JSContext* cx, - Handle item) { +NormalizedTimeDuration +js::temporal::NormalizedTimeDurationFromEpochNanosecondsDifference( + const Instant& one, const Instant& two) { + MOZ_ASSERT(IsValidEpochInstant(one)); + MOZ_ASSERT(IsValidEpochInstant(two)); + // Step 1. - if (item.isObject()) { - JSObject* itemObj = &item.toObject(); - if (itemObj->canUnwrapAs()) { - return itemObj; - } - } + auto result = one - two; // Step 2. - Duration result; - if (!ToTemporalDurationRecord(cx, item, &result)) { - return nullptr; - } + MOZ_ASSERT(IsValidInstantSpan(result)); // Step 3. - return CreateTemporalDuration(cx, result); + return result.to(); } /** - * ToTemporalDuration ( item ) + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) */ -bool js::temporal::ToTemporalDuration(JSContext* cx, Handle item, - Duration* result) { - auto obj = ToTemporalDuration(cx, item); - if (!obj) { - return false; - } +bool js::temporal::IsValidDuration(const Duration& duration) { + MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - *result = ToDuration(&obj.unwrap()); - return true; -} + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; -/** - * DaysUntil ( earlier, later ) - */ -int32_t js::temporal::DaysUntil(const PlainDate& earlier, - const PlainDate& later) { - MOZ_ASSERT(ISODateTimeWithinLimits(earlier)); - MOZ_ASSERT(ISODateTimeWithinLimits(later)); + // Step 1. + int32_t sign = DurationSign(duration); - // Steps 1-2. - int32_t epochDaysEarlier = MakeDay(earlier); - MOZ_ASSERT(std::abs(epochDaysEarlier) <= 100'000'000); + // Step 2. + for (auto v : {years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds}) { + // Step 2.a. + if (!std::isfinite(v)) { + return false; + } - // Steps 3-4. - int32_t epochDaysLater = MakeDay(later); - MOZ_ASSERT(std::abs(epochDaysLater) <= 100'000'000); + // Step 2.b. + if (v < 0 && sign > 0) { + return false; + } - // Step 5. - return epochDaysLater - epochDaysEarlier; -} + // Step 2.c. + if (v > 0 && sign < 0) { + return false; + } + } -/** - * MoveRelativeDate ( calendarRec, relativeTo, duration ) - */ -static bool MoveRelativeDate( - JSContext* cx, Handle calendar, - Handle> relativeTo, const Duration& duration, - MutableHandle> relativeToResult, - int32_t* daysResult) { - auto* unwrappedRelativeTo = relativeTo.unwrap(cx); - if (!unwrappedRelativeTo) { + // Step 3. + if (std::abs(years) >= double(int64_t(1) << 32)) { return false; } - auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 1. - auto newDate = AddDate(cx, calendar, relativeTo, duration); - if (!newDate) { + // Step 4. + if (std::abs(months) >= double(int64_t(1) << 32)) { return false; } - auto later = ToPlainDate(&newDate.unwrap()); - relativeToResult.set(newDate); - // Step 2. - *daysResult = DaysUntil(relativeToDate, later); - MOZ_ASSERT(std::abs(*daysResult) <= 200'000'000); + // Step 5. + if (std::abs(weeks) >= double(int64_t(1) << 32)) { + return false; + } - // Step 3. + // Steps 6-8. + if (!NormalizeSeconds(duration)) { + return false; + } + + // Step 9. return true; } +#ifdef DEBUG /** - * MoveRelativeZonedDateTime ( zonedDateTime, calendarRec, timeZoneRec, years, - * months, weeks, days, precalculatedPlainDateTime ) - */ -static bool MoveRelativeZonedDateTime( - JSContext* cx, Handle zonedDateTime, - Handle calendar, Handle timeZone, - const Duration& duration, - mozilla::Maybe precalculatedPlainDateTime, - MutableHandle result) { - // Step 1. - MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( - timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool js::temporal::IsValidDuration(const DateDuration& duration) { + return IsValidDuration(duration.toDuration()); +} - // Step 2. - MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( - timeZone, TimeZoneMethod::GetPossibleInstantsFor)); +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool js::temporal::IsValidDuration(const NormalizedDuration& duration) { + return IsValidDuration(duration.date) && + IsValidNormalizedTimeDuration(duration.time) && + (DurationSign(duration.date) * + NormalizedTimeDurationSign(duration.time) >= + 0); +} +#endif - // Step 3. - Instant intermediateNs; - if (precalculatedPlainDateTime) { - if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, - duration.date(), *precalculatedPlainDateTime, - &intermediateNs)) { - return false; - } - } else { - if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, - duration.date(), &intermediateNs)) { - return false; - } - } - MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); +static bool ThrowInvalidDurationPart(JSContext* cx, double value, + const char* name, unsigned errorNumber) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, value); - // Step 4. - result.set(ZonedDateTime{intermediateNs, zonedDateTime.timeZone(), - zonedDateTime.calendar()}); - return true; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber, name, + numStr); + return false; } /** - * TotalDurationNanoseconds ( hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) */ -static mozilla::Maybe TotalDurationNanoseconds( - const Duration& duration) { - // Our implementation supports |duration.days| to avoid computing |days * 24| - // in the caller, which may not be representable as a double value. - int64_t days; - if (!mozilla::NumberEqualsInt64(duration.days, &days)) { - return mozilla::Nothing(); - } - int64_t hours; - if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { - return mozilla::Nothing(); - } - mozilla::CheckedInt64 result = days; - result *= 24; - result += hours; +bool js::temporal::ThrowIfInvalidDuration(JSContext* cx, + const Duration& duration) { + MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); + + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Step 1. - int64_t minutes; - if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { - return mozilla::Nothing(); - } - result *= 60; - result += minutes; + int32_t sign = DurationSign(duration); + + auto throwIfInvalid = [&](double v, const char* name) { + // Step 2.a. + if (!std::isfinite(v)) { + return ThrowInvalidDurationPart( + cx, v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + + // Steps 2.b-c. + if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) { + return ThrowInvalidDurationPart(cx, v, name, + JSMSG_TEMPORAL_DURATION_INVALID_SIGN); + } + + return true; + }; + + auto throwIfTooLarge = [&](double v, const char* name) { + if (std::abs(v) >= double(int64_t(1) << 32)) { + return ThrowInvalidDurationPart( + cx, v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + return true; + }; // Step 2. - int64_t seconds; - if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { - return mozilla::Nothing(); + if (!throwIfInvalid(years, "years")) { + return false; + } + if (!throwIfInvalid(months, "months")) { + return false; + } + if (!throwIfInvalid(weeks, "weeks")) { + return false; + } + if (!throwIfInvalid(days, "days")) { + return false; + } + if (!throwIfInvalid(hours, "hours")) { + return false; + } + if (!throwIfInvalid(minutes, "minutes")) { + return false; + } + if (!throwIfInvalid(seconds, "seconds")) { + return false; + } + if (!throwIfInvalid(milliseconds, "milliseconds")) { + return false; + } + if (!throwIfInvalid(microseconds, "microseconds")) { + return false; + } + if (!throwIfInvalid(nanoseconds, "nanoseconds")) { + return false; } - result *= 60; - result += seconds; // Step 3. - int64_t milliseconds; - if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { - return mozilla::Nothing(); + if (!throwIfTooLarge(years, "years")) { + return false; } - result *= 1000; - result += milliseconds; // Step 4. - int64_t microseconds; - if (!mozilla::NumberEqualsInt64(duration.microseconds, µseconds)) { - return mozilla::Nothing(); + if (!throwIfTooLarge(months, "months")) { + return false; } - result *= 1000; - result += microseconds; // Step 5. - int64_t nanoseconds; - if (!mozilla::NumberEqualsInt64(duration.nanoseconds, &nanoseconds)) { - return mozilla::Nothing(); + if (!throwIfTooLarge(weeks, "weeks")) { + return false; } - result *= 1000; - result += nanoseconds; - // Step 5 (Return). - if (!result.isValid()) { - return mozilla::Nothing(); + // Steps 6-8. + if (!NormalizeSeconds(duration)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; } - return mozilla::Some(result.value()); + + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 9. + return true; } /** - * TotalDurationNanoseconds ( hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) */ -static BigInt* TotalDurationNanosecondsSlow(JSContext* cx, - const Duration& duration) { - // Our implementation supports |duration.days| to avoid computing |days * 24| - // in the caller, which may not be representable as a double value. - Rooted result(cx, BigInt::createFromDouble(cx, duration.days)); - if (!result) { - return nullptr; - } +bool js::temporal::ThrowIfInvalidDuration(JSContext* cx, + const DateDuration& duration) { + const auto& [years, months, weeks, days] = duration; - Rooted temp(cx); - auto multiplyAdd = [&](int32_t factor, double number) { - temp = BigInt::createFromInt64(cx, factor); - if (!temp) { - return false; - } + // Step 1. + int32_t sign = DurationSign(duration); - result = BigInt::mul(cx, result, temp); - if (!result) { - return false; - } + auto throwIfInvalid = [&](int64_t v, const char* name) { + // Step 2.a. (Not applicable) - temp = BigInt::createFromDouble(cx, number); - if (!temp) { - return false; + // Steps 2.b-c. + if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) { + return ThrowInvalidDurationPart(cx, double(v), name, + JSMSG_TEMPORAL_DURATION_INVALID_SIGN); } - result = BigInt::add(cx, result, temp); - return !!result; + return true; }; - if (!multiplyAdd(24, duration.hours)) { - return nullptr; - } - - // Step 1. - if (!multiplyAdd(60, duration.minutes)) { - return nullptr; - } + auto throwIfTooLarge = [&](int64_t v, const char* name) { + if (std::abs(v) >= (int64_t(1) << 32)) { + return ThrowInvalidDurationPart( + cx, double(v), name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + return true; + }; // Step 2. - if (!multiplyAdd(60, duration.seconds)) { - return nullptr; + if (!throwIfInvalid(years, "years")) { + return false; + } + if (!throwIfInvalid(months, "months")) { + return false; + } + if (!throwIfInvalid(weeks, "weeks")) { + return false; + } + if (!throwIfInvalid(days, "days")) { + return false; } // Step 3. - if (!multiplyAdd(1000, duration.milliseconds)) { - return nullptr; + if (!throwIfTooLarge(years, "years")) { + return false; } // Step 4. - if (!multiplyAdd(1000, duration.microseconds)) { - return nullptr; + if (!throwIfTooLarge(months, "months")) { + return false; } // Step 5. - if (!multiplyAdd(1000, duration.nanoseconds)) { - return nullptr; + if (!throwIfTooLarge(weeks, "weeks")) { + return false; } - // Step 5 (Return). - return result; -} - -struct NanosecondsAndDays final { - int32_t days = 0; - int64_t nanoseconds = 0; -}; - -/** - * Split duration into full days and remainding nanoseconds. - */ -static ::NanosecondsAndDays NanosecondsToDays(int64_t nanoseconds) { - constexpr int64_t dayLengthNs = ToNanoseconds(TemporalUnit::Day); + // Steps 6-8. + if (std::abs(days) > ((int64_t(1) << 53) / 86400)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } - static_assert(INT64_MAX / dayLengthNs <= INT32_MAX, - "days doesn't exceed INT32_MAX"); + MOZ_ASSERT(IsValidDuration(duration)); - return {int32_t(nanoseconds / dayLengthNs), nanoseconds % dayLengthNs}; + // Step 9. + return true; } /** - * Split duration into full days and remainding nanoseconds. + * DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, + * seconds, milliseconds, microseconds ) */ -static bool NanosecondsToDaysSlow( - JSContext* cx, Handle nanoseconds, - MutableHandle result) { - constexpr int64_t dayLengthNs = ToNanoseconds(TemporalUnit::Day); +static TemporalUnit DefaultTemporalLargestUnit(const Duration& duration) { + MOZ_ASSERT(IsIntegerDuration(duration)); - Rooted dayLength(cx, BigInt::createFromInt64(cx, dayLengthNs)); - if (!dayLength) { - return false; + // Step 1. + if (duration.years != 0) { + return TemporalUnit::Year; } - Rooted days(cx); - Rooted nanos(cx); - if (!BigInt::divmod(cx, nanoseconds, dayLength, &days, &nanos)) { - return false; + // Step 2. + if (duration.months != 0) { + return TemporalUnit::Month; } - result.set(temporal::NanosecondsAndDays::from( - days, ToInstantSpan(nanos), InstantSpan::fromNanoseconds(dayLengthNs))); - return true; -} - -/** - * Split duration into full days and remainding nanoseconds. - */ -static bool NanosecondsToDays( - JSContext* cx, const Duration& duration, - MutableHandle result) { - if (auto total = TotalDurationNanoseconds(duration.time())) { - auto nanosAndDays = ::NanosecondsToDays(*total); - - result.set(temporal::NanosecondsAndDays::from( - nanosAndDays.days, - InstantSpan::fromNanoseconds(nanosAndDays.nanoseconds), - InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day)))); - return true; + // Step 3. + if (duration.weeks != 0) { + return TemporalUnit::Week; } - Rooted nanoseconds( - cx, TotalDurationNanosecondsSlow(cx, duration.time())); - if (!nanoseconds) { - return false; + // Step 4. + if (duration.days != 0) { + return TemporalUnit::Day; } - return ::NanosecondsToDaysSlow(cx, nanoseconds, result); -} + // Step 5. + if (duration.hours != 0) { + return TemporalUnit::Hour; + } -/** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , - * precalculatedPlainDateTime ] ) - */ -static bool NanosecondsToDays( - JSContext* cx, const Duration& duration, - Handle zonedRelativeTo, Handle timeZone, - MutableHandle result) { - if (auto total = TotalDurationNanoseconds(duration.time())) { - auto nanoseconds = InstantSpan::fromNanoseconds(*total); - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + // Step 6. + if (duration.minutes != 0) { + return TemporalUnit::Minute; + } - return NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, - result); + // Step 7. + if (duration.seconds != 0) { + return TemporalUnit::Second; } - auto* nanoseconds = TotalDurationNanosecondsSlow(cx, duration.time()); - if (!nanoseconds) { - return false; + // Step 8. + if (duration.milliseconds != 0) { + return TemporalUnit::Millisecond; } - // NanosecondsToDays, step 6. - if (!IsValidInstantSpan(nanoseconds)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_INSTANT_INVALID); - return false; + // Step 9. + if (duration.microseconds != 0) { + return TemporalUnit::Microsecond; } - return NanosecondsToDays(cx, ToInstantSpan(nanoseconds), zonedRelativeTo, - timeZone, result); + // Step 10. + return TemporalUnit::Nanosecond; } /** - * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) + * CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds [ , newTarget ] ) */ -static TimeDuration CreateTimeDurationRecord(int64_t days, int64_t hours, - int64_t minutes, int64_t seconds, - int64_t milliseconds, - int64_t microseconds, - int64_t nanoseconds) { - // Step 1. - MOZ_ASSERT(IsValidDuration({0, 0, 0, double(days), double(hours), - double(minutes), double(seconds), - double(microseconds), double(nanoseconds)})); - - // Step 2. - return { - double(days), double(hours), double(minutes), - double(seconds), double(milliseconds), double(microseconds), - double(nanoseconds), - }; -} +static DurationObject* CreateTemporalDuration(JSContext* cx, + const CallArgs& args, + const Duration& duration) { + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; -/** - * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) - */ -static TimeDuration CreateTimeDurationRecord(double days, double hours, - double minutes, double seconds, - double milliseconds, - double microseconds, - double nanoseconds) { // Step 1. - MOZ_ASSERT(IsValidDuration({0, 0, 0, days, hours, minutes, seconds, - milliseconds, microseconds, nanoseconds})); + if (!ThrowIfInvalidDuration(cx, duration)) { + return nullptr; + } - // Step 2. - // NB: Adds +0.0 to correctly handle negative zero. - return { - days + (+0.0), hours + (+0.0), minutes + (+0.0), - seconds + (+0.0), milliseconds + (+0.0), microseconds + (+0.0), - nanoseconds + (+0.0), - }; -} + // Steps 2-3. + Rooted proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Duration, &proto)) { + return nullptr; + } -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - * - * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit ) - */ -static TimeDuration BalanceTimeDuration(int64_t nanoseconds, - TemporalUnit largestUnit) { - // Step 1. (Handled in caller.) - - // Step 2. - int64_t days = 0; - int64_t hours = 0; - int64_t minutes = 0; - int64_t seconds = 0; - int64_t milliseconds = 0; - int64_t microseconds = 0; - - // Steps 3-4. (Not applicable in our implementation.) - // - // We don't need to convert to positive numbers, because integer division - // truncates and the %-operator has modulo semantics. - - // Steps 5-11. - switch (largestUnit) { - // Step 5. - case TemporalUnit::Year: - case TemporalUnit::Month: - case TemporalUnit::Week: - case TemporalUnit::Day: { - // Step 5.a. - microseconds = nanoseconds / 1000; - - // Step 5.b. - nanoseconds = nanoseconds % 1000; - - // Step 5.c. - milliseconds = microseconds / 1000; - - // Step 5.d. - microseconds = microseconds % 1000; - - // Step 5.e. - seconds = milliseconds / 1000; - - // Step 5.f. - milliseconds = milliseconds % 1000; - - // Step 5.g. - minutes = seconds / 60; - - // Step 5.h. - seconds = seconds % 60; - - // Step 5.i. - hours = minutes / 60; - - // Step 5.j. - minutes = minutes % 60; - - // Step 5.k. - days = hours / 24; - - // Step 5.l. - hours = hours % 24; - - break; - } - - case TemporalUnit::Hour: { - // Step 6.a. - microseconds = nanoseconds / 1000; - - // Step 6.b. - nanoseconds = nanoseconds % 1000; - - // Step 6.c. - milliseconds = microseconds / 1000; - - // Step 6.d. - microseconds = microseconds % 1000; - - // Step 6.e. - seconds = milliseconds / 1000; - - // Step 6.f. - milliseconds = milliseconds % 1000; - - // Step 6.g. - minutes = seconds / 60; - - // Step 6.h. - seconds = seconds % 60; - - // Step 6.i. - hours = minutes / 60; - - // Step 6.j. - minutes = minutes % 60; - - break; - } - - // Step 7. - case TemporalUnit::Minute: { - // Step 7.a. - microseconds = nanoseconds / 1000; - - // Step 7.b. - nanoseconds = nanoseconds % 1000; - - // Step 7.c. - milliseconds = microseconds / 1000; - - // Step 7.d. - microseconds = microseconds % 1000; - - // Step 7.e. - seconds = milliseconds / 1000; - - // Step 7.f. - milliseconds = milliseconds % 1000; - - // Step 7.g. - minutes = seconds / 60; - - // Step 7.h. - seconds = seconds % 60; - - break; - } - - // Step 8. - case TemporalUnit::Second: { - // Step 8.a. - microseconds = nanoseconds / 1000; - - // Step 8.b. - nanoseconds = nanoseconds % 1000; - - // Step 8.c. - milliseconds = microseconds / 1000; - - // Step 8.d. - microseconds = microseconds % 1000; - - // Step 8.e. - seconds = milliseconds / 1000; - - // Step 8.f. - milliseconds = milliseconds % 1000; - - break; - } - - // Step 9. - case TemporalUnit::Millisecond: { - // Step 9.a. - microseconds = nanoseconds / 1000; - - // Step 9.b. - nanoseconds = nanoseconds % 1000; - - // Step 9.c. - milliseconds = microseconds / 1000; - - // Step 9.d. - microseconds = microseconds % 1000; + auto* object = NewObjectWithClassProto(cx, proto); + if (!object) { + return nullptr; + } - break; - } + // Steps 4-13. + // Add zero to convert -0 to +0. + object->setFixedSlot(DurationObject::YEARS_SLOT, NumberValue(years + (+0.0))); + object->setFixedSlot(DurationObject::MONTHS_SLOT, + NumberValue(months + (+0.0))); + object->setFixedSlot(DurationObject::WEEKS_SLOT, NumberValue(weeks + (+0.0))); + object->setFixedSlot(DurationObject::DAYS_SLOT, NumberValue(days + (+0.0))); + object->setFixedSlot(DurationObject::HOURS_SLOT, NumberValue(hours + (+0.0))); + object->setFixedSlot(DurationObject::MINUTES_SLOT, + NumberValue(minutes + (+0.0))); + object->setFixedSlot(DurationObject::SECONDS_SLOT, + NumberValue(seconds + (+0.0))); + object->setFixedSlot(DurationObject::MILLISECONDS_SLOT, + NumberValue(milliseconds + (+0.0))); + object->setFixedSlot(DurationObject::MICROSECONDS_SLOT, + NumberValue(microseconds + (+0.0))); + object->setFixedSlot(DurationObject::NANOSECONDS_SLOT, + NumberValue(nanoseconds + (+0.0))); - // Step 10. - case TemporalUnit::Microsecond: { - // Step 10.a. - microseconds = nanoseconds / 1000; + // Step 14. + return object; +} - // Step 10.b. - nanoseconds = nanoseconds % 1000; +/** + * CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds [ , newTarget ] ) + */ +DurationObject* js::temporal::CreateTemporalDuration(JSContext* cx, + const Duration& duration) { + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; - break; - } + MOZ_ASSERT(IsInteger(years)); + MOZ_ASSERT(IsInteger(months)); + MOZ_ASSERT(IsInteger(weeks)); + MOZ_ASSERT(IsInteger(days)); + MOZ_ASSERT(IsInteger(hours)); + MOZ_ASSERT(IsInteger(minutes)); + MOZ_ASSERT(IsInteger(seconds)); + MOZ_ASSERT(IsInteger(milliseconds)); + MOZ_ASSERT(IsInteger(microseconds)); + MOZ_ASSERT(IsInteger(nanoseconds)); - // Step 11. - case TemporalUnit::Nanosecond: { - // Nothing to do. - break; - } + // Step 1. + if (!ThrowIfInvalidDuration(cx, duration)) { + return nullptr; + } - case TemporalUnit::Auto: - MOZ_CRASH("Unexpected temporal unit"); + // Steps 2-3. + auto* object = NewBuiltinClassInstance(cx); + if (!object) { + return nullptr; } - // Step 12. (Not applicable, all values are finite) + // Steps 4-13. + // Add zero to convert -0 to +0. + object->setFixedSlot(DurationObject::YEARS_SLOT, NumberValue(years + (+0.0))); + object->setFixedSlot(DurationObject::MONTHS_SLOT, + NumberValue(months + (+0.0))); + object->setFixedSlot(DurationObject::WEEKS_SLOT, NumberValue(weeks + (+0.0))); + object->setFixedSlot(DurationObject::DAYS_SLOT, NumberValue(days + (+0.0))); + object->setFixedSlot(DurationObject::HOURS_SLOT, NumberValue(hours + (+0.0))); + object->setFixedSlot(DurationObject::MINUTES_SLOT, + NumberValue(minutes + (+0.0))); + object->setFixedSlot(DurationObject::SECONDS_SLOT, + NumberValue(seconds + (+0.0))); + object->setFixedSlot(DurationObject::MILLISECONDS_SLOT, + NumberValue(milliseconds + (+0.0))); + object->setFixedSlot(DurationObject::MICROSECONDS_SLOT, + NumberValue(microseconds + (+0.0))); + object->setFixedSlot(DurationObject::NANOSECONDS_SLOT, + NumberValue(nanoseconds + (+0.0))); - // Step 13. - return CreateTimeDurationRecord(days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds); + // Step 14. + return object; } /** - * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit ) + * ToIntegerIfIntegral ( argument ) */ -static bool BalancePossiblyInfiniteTimeDurationSlow(JSContext* cx, - Handle nanos, - TemporalUnit largestUnit, - TimeDuration* result) { - // Steps 1-2. (Handled in caller.) - - BigInt* zero = BigInt::zero(cx); - if (!zero) { +static bool ToIntegerIfIntegral(JSContext* cx, const char* name, + Handle argument, double* num) { + // Step 1. + double d; + if (!JS::ToNumber(cx, argument, &d)) { return false; } - // Step 3. - Rooted days(cx, zero); - Rooted hours(cx, zero); - Rooted minutes(cx, zero); - Rooted seconds(cx, zero); - Rooted milliseconds(cx, zero); - Rooted microseconds(cx, zero); - Rooted nanoseconds(cx, nanos); - - // Steps 4-5. - // - // We don't need to convert to positive numbers, because BigInt division - // truncates and BigInt modulo has modulo semantics. + // Step 2. + if (!js::IsInteger(d)) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, d); - // Steps 6-12. - Rooted thousand(cx, BigInt::createFromInt64(cx, 1000)); - if (!thousand) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_NOT_INTEGER, numStr, + name); return false; } - Rooted sixty(cx, BigInt::createFromInt64(cx, 60)); - if (!sixty) { - return false; - } + // Step 3. + *num = d; + return true; +} - Rooted twentyfour(cx, BigInt::createFromInt64(cx, 24)); - if (!twentyfour) { +/** + * ToIntegerIfIntegral ( argument ) + */ +static bool ToIntegerIfIntegral(JSContext* cx, Handle name, + Handle argument, double* result) { + // Step 1. + double d; + if (!JS::ToNumber(cx, argument, &d)) { return false; } - switch (largestUnit) { - // Step 6. - case TemporalUnit::Year: - case TemporalUnit::Month: - case TemporalUnit::Week: - case TemporalUnit::Day: { - // Steps 6.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - // Steps 6.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } - - // Steps 6.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } - - // Steps 6.g-h. - if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { - return false; - } - - // Steps 6.i-j. - if (!BigInt::divmod(cx, minutes, sixty, &hours, &minutes)) { - return false; - } - - // Steps 6.k-l. - if (!BigInt::divmod(cx, hours, twentyfour, &days, &hours)) { - return false; - } - - break; - } - - // Step 7. - case TemporalUnit::Hour: { - // Steps 7.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - // Steps 7.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } - - // Steps 7.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } - - // Steps 7.g-h. - if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { - return false; - } - - // Steps 7.i-j. - if (!BigInt::divmod(cx, minutes, sixty, &hours, &minutes)) { - return false; - } - - break; - } - - // Step 8. - case TemporalUnit::Minute: { - // Steps 8.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - // Steps 8.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } - - // Steps 8.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } - - // Steps 8.g-h. - if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { - return false; - } - - break; - } - - // Step 9. - case TemporalUnit::Second: { - // Steps 9.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - // Steps 9.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } - - // Steps 9.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } - - break; - } - - // Step 10. - case TemporalUnit::Millisecond: { - // Steps 10.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - // Steps 10.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } - - break; - } - - // Step 11. - case TemporalUnit::Microsecond: { - // Steps 11.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - break; - } - - // Step 12. - case TemporalUnit::Nanosecond: { - // Nothing to do. - break; - } - - case TemporalUnit::Auto: - MOZ_CRASH("Unexpected temporal unit"); - } - - double daysNumber = BigInt::numberValue(days); - double hoursNumber = BigInt::numberValue(hours); - double minutesNumber = BigInt::numberValue(minutes); - double secondsNumber = BigInt::numberValue(seconds); - double millisecondsNumber = BigInt::numberValue(milliseconds); - double microsecondsNumber = BigInt::numberValue(microseconds); - double nanosecondsNumber = BigInt::numberValue(nanoseconds); + // Step 2. + if (!js::IsInteger(d)) { + if (auto nameStr = js::QuoteString(cx, name)) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, d); - // Step 13. - for (double v : {daysNumber, hoursNumber, minutesNumber, secondsNumber, - millisecondsNumber, microsecondsNumber, nanosecondsNumber}) { - if (std::isinf(v)) { - *result = { - daysNumber, hoursNumber, minutesNumber, - secondsNumber, millisecondsNumber, microsecondsNumber, - nanosecondsNumber, - }; - return true; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_NOT_INTEGER, numStr, + nameStr.get()); } + return false; } - // Step 14. - *result = CreateTimeDurationRecord(daysNumber, hoursNumber, minutesNumber, - secondsNumber, millisecondsNumber, - microsecondsNumber, nanosecondsNumber); + // Step 3. + *result = d; return true; } /** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) + * ToTemporalPartialDurationRecord ( temporalDurationLike ) */ -static bool BalanceTimeDurationSlow(JSContext* cx, Handle nanoseconds, - TemporalUnit largestUnit, - TimeDuration* result) { - // Step 1. - if (!BalancePossiblyInfiniteTimeDurationSlow(cx, nanoseconds, largestUnit, - result)) { - return false; - } +static bool ToTemporalPartialDurationRecord( + JSContext* cx, Handle temporalDurationLike, Duration* result) { + // Steps 1-3. (Not applicable in our implementation.) + + Rooted value(cx); + bool any = false; + + auto getDurationProperty = [&](Handle name, double* num) { + if (!GetProperty(cx, temporalDurationLike, temporalDurationLike, name, + &value)) { + return false; + } - // Steps 2-3. - return ThrowIfInvalidDuration(cx, result->toDuration()); -} + if (!value.isUndefined()) { + any = true; -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -static bool BalanceTimeDuration(JSContext* cx, const Duration& one, - const Duration& two, TemporalUnit largestUnit, - TimeDuration* result) { - MOZ_ASSERT(IsValidDuration(one)); - MOZ_ASSERT(IsValidDuration(two)); - MOZ_ASSERT(largestUnit >= TemporalUnit::Day); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto oneNanoseconds = TotalDurationNanoseconds(one)) { - if (auto twoNanoseconds = TotalDurationNanoseconds(two)) { - mozilla::CheckedInt64 nanoseconds = *oneNanoseconds; - nanoseconds += *twoNanoseconds; - if (nanoseconds.isValid()) { - *result = ::BalanceTimeDuration(nanoseconds.value(), largestUnit); - return true; + if (!ToIntegerIfIntegral(cx, name, value, num)) { + return false; } } - } + return true; + }; - Rooted oneNanoseconds(cx, TotalDurationNanosecondsSlow(cx, one)); - if (!oneNanoseconds) { + // Steps 4-23. + if (!getDurationProperty(cx->names().days, &result->days)) { return false; } - - Rooted twoNanoseconds(cx, TotalDurationNanosecondsSlow(cx, two)); - if (!twoNanoseconds) { + if (!getDurationProperty(cx->names().hours, &result->hours)) { + return false; + } + if (!getDurationProperty(cx->names().microseconds, &result->microseconds)) { + return false; + } + if (!getDurationProperty(cx->names().milliseconds, &result->milliseconds)) { + return false; + } + if (!getDurationProperty(cx->names().minutes, &result->minutes)) { + return false; + } + if (!getDurationProperty(cx->names().months, &result->months)) { + return false; + } + if (!getDurationProperty(cx->names().nanoseconds, &result->nanoseconds)) { + return false; + } + if (!getDurationProperty(cx->names().seconds, &result->seconds)) { + return false; + } + if (!getDurationProperty(cx->names().weeks, &result->weeks)) { + return false; + } + if (!getDurationProperty(cx->names().years, &result->years)) { return false; } - Rooted nanoseconds(cx, - BigInt::add(cx, oneNanoseconds, twoNanoseconds)); - if (!nanoseconds) { + // Step 24. + if (!any) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_MISSING_UNIT); return false; } - return BalanceTimeDurationSlow(cx, nanoseconds, largestUnit, result); + // Step 25. + return true; } /** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) + * ToTemporalDurationRecord ( temporalDurationLike ) */ -static bool BalanceTimeDuration(JSContext* cx, double days, const Duration& one, - const Duration& two, TemporalUnit largestUnit, - TimeDuration* result) { - MOZ_ASSERT(IsInteger(days)); - MOZ_ASSERT(IsValidDuration(one)); - MOZ_ASSERT(IsValidDuration(two)); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto oneNanoseconds = TotalDurationNanoseconds(one)) { - if (auto twoNanoseconds = TotalDurationNanoseconds(two)) { - int64_t intDays; - if (mozilla::NumberEqualsInt64(days, &intDays)) { - mozilla::CheckedInt64 daysNanoseconds = intDays; - daysNanoseconds *= ToNanoseconds(TemporalUnit::Day); - - mozilla::CheckedInt64 nanoseconds = *oneNanoseconds; - nanoseconds += *twoNanoseconds; - nanoseconds += daysNanoseconds; - - if (nanoseconds.isValid()) { - *result = ::BalanceTimeDuration(nanoseconds.value(), largestUnit); - return true; - } - } +bool js::temporal::ToTemporalDurationRecord(JSContext* cx, + Handle temporalDurationLike, + Duration* result) { + // Step 1. + if (!temporalDurationLike.isObject()) { + // Step 1.a. + if (!temporalDurationLike.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, + temporalDurationLike, nullptr, "not a string"); + return false; } - } + Rooted string(cx, temporalDurationLike.toString()); - Rooted oneNanoseconds(cx, TotalDurationNanosecondsSlow(cx, one)); - if (!oneNanoseconds) { - return false; + // Step 1.b. + return ParseTemporalDurationString(cx, string, result); } - Rooted twoNanoseconds(cx, TotalDurationNanosecondsSlow(cx, two)); - if (!twoNanoseconds) { - return false; + Rooted durationLike(cx, &temporalDurationLike.toObject()); + + // Step 2. + if (auto* duration = durationLike->maybeUnwrapIf()) { + *result = ToDuration(duration); + return true; } - Rooted nanoseconds(cx, - BigInt::add(cx, oneNanoseconds, twoNanoseconds)); - if (!nanoseconds) { + // Step 3. + Duration duration = {}; + + // Steps 4-14. + if (!ToTemporalPartialDurationRecord(cx, durationLike, &duration)) { return false; } - if (days) { - Rooted daysNanoseconds( - cx, TotalDurationNanosecondsSlow(cx, {0, 0, 0, days})); - if (!daysNanoseconds) { - return false; - } - - nanoseconds = BigInt::add(cx, nanoseconds, daysNanoseconds); - if (!nanoseconds) { - return false; - } + // Step 15. + if (!ThrowIfInvalidDuration(cx, duration)) { + return false; } - return BalanceTimeDurationSlow(cx, nanoseconds, largestUnit, result); + // Step 16. + *result = duration; + return true; } /** - * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit ) + * ToTemporalDuration ( item ) */ -static bool BalancePossiblyInfiniteTimeDuration(JSContext* cx, - const Duration& duration, - TemporalUnit largestUnit, - TimeDuration* result) { - // NB: |duration.days| can have a different sign than the time components. - MOZ_ASSERT(IsValidDuration(duration.time())); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto nanoseconds = TotalDurationNanoseconds(duration)) { - *result = ::BalanceTimeDuration(*nanoseconds, largestUnit); - return true; +Wrapped js::temporal::ToTemporalDuration(JSContext* cx, + Handle item) { + // Step 1. + if (item.isObject()) { + JSObject* itemObj = &item.toObject(); + if (itemObj->canUnwrapAs()) { + return itemObj; + } } - // Steps 1-2. - Rooted nanoseconds(cx, TotalDurationNanosecondsSlow(cx, duration)); - if (!nanoseconds) { - return false; + // Step 2. + Duration result; + if (!ToTemporalDurationRecord(cx, item, &result)) { + return nullptr; } - // Steps 3-14. - return ::BalancePossiblyInfiniteTimeDurationSlow(cx, nanoseconds, largestUnit, - result); + // Step 3. + return CreateTemporalDuration(cx, result); } /** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) + * ToTemporalDuration ( item ) */ -bool js::temporal::BalanceTimeDuration(JSContext* cx, const Duration& duration, - TemporalUnit largestUnit, - TimeDuration* result) { - if (!::BalancePossiblyInfiniteTimeDuration(cx, duration, largestUnit, - result)) { +bool js::temporal::ToTemporalDuration(JSContext* cx, Handle item, + Duration* result) { + auto obj = ToTemporalDuration(cx, item); + if (!obj) { return false; } - return ThrowIfInvalidDuration(cx, result->toDuration()); + + *result = ToDuration(&obj.unwrap()); + return true; } /** - * BalancePossiblyInfiniteTimeDurationRelative ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit, zonedRelativeTo, - * timeZoneRec [ , precalculatedPlainDateTime ] ) + * DaysUntil ( earlier, later ) */ -static bool BalancePossiblyInfiniteTimeDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - Handle relativeTo, Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - TimeDuration* result) { - // Step 1. (Not applicable) - - // Step 2. - auto intermediateNs = relativeTo.instant(); - - // Step 3. - const auto& startInstant = relativeTo.instant(); +int32_t js::temporal::DaysUntil(const PlainDate& earlier, + const PlainDate& later) { + MOZ_ASSERT(ISODateTimeWithinLimits(earlier)); + MOZ_ASSERT(ISODateTimeWithinLimits(later)); - // Step 4. - PlainDateTime startDateTime; - if (duration.days != 0) { - // Step 4.a. - if (!precalculatedPlainDateTime) { - if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { - return false; - } - precalculatedPlainDateTime = - mozilla::SomeRef(startDateTime); - } + // Steps 1-2. + int32_t epochDaysEarlier = MakeDay(earlier); + MOZ_ASSERT(MinEpochDay <= epochDaysEarlier && + epochDaysEarlier <= MaxEpochDay); - // Steps 4.b-c. - Rooted isoCalendar(cx, CalendarValue(cx->names().iso8601)); - if (!AddDaysToZonedDateTime(cx, startInstant, *precalculatedPlainDateTime, - timeZone, isoCalendar, duration.days, - &intermediateNs)) { - return false; - } - } + // Steps 3-4. + int32_t epochDaysLater = MakeDay(later); + MOZ_ASSERT(MinEpochDay <= epochDaysLater && epochDaysLater <= MaxEpochDay); // Step 5. - Instant endNs; - if (!AddInstant(cx, intermediateNs, duration.time(), &endNs)) { + return epochDaysLater - epochDaysEarlier; +} + +/** + * MoveRelativeDate ( calendarRec, relativeTo, duration ) + */ +static bool MoveRelativeDate( + JSContext* cx, Handle calendar, + Handle> relativeTo, const DateDuration& duration, + MutableHandle> relativeToResult, + int32_t* daysResult) { + auto* unwrappedRelativeTo = relativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { return false; } - MOZ_ASSERT(IsValidEpochInstant(endNs)); - - // Step 6. - auto nanoseconds = endNs - relativeTo.instant(); - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 7. - if (nanoseconds == InstantSpan{}) { - *result = {}; - return true; + // Step 1. + auto newDate = AddDate(cx, calendar, relativeTo, duration); + if (!newDate) { + return false; } + auto later = ToPlainDate(&newDate.unwrap()); + relativeToResult.set(newDate); - // Steps 8-9. - double days = 0; - if (TemporalUnit::Year <= largestUnit && largestUnit <= TemporalUnit::Day) { - // Step 8.a. - if (!precalculatedPlainDateTime) { - if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { - return false; - } - precalculatedPlainDateTime = - mozilla::SomeRef(startDateTime); - } - - // Step 8.b. - Rooted nanosAndDays(cx); - if (!NanosecondsToDays(cx, nanoseconds, relativeTo, timeZone, - *precalculatedPlainDateTime, &nanosAndDays)) { - return false; - } - - // NB: |days| is passed to CreateTimeDurationRecord, which performs - // |ℝ(𝔽(days))|, so it's safe to convert from BigInt to double here. - - // Step 8.c. - days = nanosAndDays.daysNumber(); - MOZ_ASSERT(IsInteger(days)); - - // FIXME: spec issue - `result.[[Nanoseconds]]` not created in all branches - // https://github.com/tc39/proposal-temporal/issues/2616 - - // Step 8.d. - nanoseconds = nanosAndDays.nanoseconds(); - MOZ_ASSERT_IF(days > 0, nanoseconds >= InstantSpan{}); - MOZ_ASSERT_IF(days < 0, nanoseconds <= InstantSpan{}); + // Step 2. + *daysResult = DaysUntil(relativeToDate, later); + MOZ_ASSERT(std::abs(*daysResult) <= MaxEpochDaysDuration); - // Step 8.e. - largestUnit = TemporalUnit::Hour; - } + // Step 3. + return true; +} - // Step 10. (Not applicable in our implementation.) +/** + * MoveRelativeZonedDateTime ( zonedDateTime, calendarRec, timeZoneRec, years, + * months, weeks, days, precalculatedPlainDateTime ) + */ +static bool MoveRelativeZonedDateTime( + JSContext* cx, Handle zonedDateTime, + Handle calendar, Handle timeZone, + const DateDuration& duration, + mozilla::Maybe precalculatedPlainDateTime, + MutableHandle result) { + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); - // Steps 11-12. - TimeDuration balanceResult; - if (auto nanos = nanoseconds.toNanoseconds(); nanos.isValid()) { - // Step 11. - balanceResult = ::BalanceTimeDuration(nanos.value(), largestUnit); + // Step 2. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); - // Step 12. - MOZ_ASSERT(IsValidDuration(balanceResult.toDuration())); - } else { - Rooted ns(cx, ToEpochNanoseconds(cx, nanoseconds)); - if (!ns) { + // Step 3. + Instant intermediateNs; + if (precalculatedPlainDateTime) { + if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, + duration, *precalculatedPlainDateTime, + &intermediateNs)) { return false; } - - // Step 11. - if (!::BalancePossiblyInfiniteTimeDurationSlow(cx, ns, largestUnit, - &balanceResult)) { + } else { + if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, + duration, &intermediateNs)) { return false; } - - // Step 12. - if (!IsValidDuration(balanceResult.toDuration())) { - *result = balanceResult; - return true; - } } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); - // Step 13. - *result = { - days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, - }; + // Step 4. + result.set(ZonedDateTime{intermediateNs, zonedDateTime.timeZone(), + zonedDateTime.calendar()}); return true; } /** - * BalancePossiblyInfiniteTimeDurationRelative ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit, zonedRelativeTo, - * timeZoneRec [ , precalculatedPlainDateTime ] ) + * Split duration into full days and remainding nanoseconds. */ -static bool BalancePossiblyInfiniteTimeDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - Handle relativeTo, Handle timeZone, - TimeDuration* result) { - return BalancePossiblyInfiniteTimeDurationRelative( - cx, duration, largestUnit, relativeTo, timeZone, mozilla::Nothing(), - result); -} +static NormalizedTimeAndDays NormalizedTimeDurationToDays( + const NormalizedTimeDuration& duration) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); -/** - * BalanceTimeDurationRelative ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit, zonedRelativeTo, timeZoneRec, - * precalculatedPlainDateTime ) - */ -static bool BalanceTimeDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - Handle relativeTo, Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - TimeDuration* result) { - // Step 1. - if (!BalancePossiblyInfiniteTimeDurationRelative( - cx, duration, largestUnit, relativeTo, timeZone, - precalculatedPlainDateTime, result)) { - return false; + auto [seconds, nanoseconds] = duration; + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= 1'000'000'000; } - // Steps 2-3. - return ThrowIfInvalidDuration(cx, result->toDuration()); -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -bool js::temporal::BalanceTimeDuration(JSContext* cx, - const InstantSpan& nanoseconds, - TemporalUnit largestUnit, - TimeDuration* result) { - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); - - // Steps 1-3. (Not applicable) + int64_t days = seconds / ToSeconds(TemporalUnit::Day); + seconds = seconds % ToSeconds(TemporalUnit::Day); - // Fast-path when we can perform the whole computation with int64 values. - if (auto nanos = nanoseconds.toNanoseconds(); nanos.isValid()) { - *result = ::BalanceTimeDuration(nanos.value(), largestUnit); - return true; - } + int64_t time = seconds * ToNanoseconds(TemporalUnit::Second) + nanoseconds; - Rooted nanos(cx, ToEpochNanoseconds(cx, nanoseconds)); - if (!nanos) { - return false; - } + constexpr int64_t dayLength = ToNanoseconds(TemporalUnit::Day); + MOZ_ASSERT(std::abs(time) < dayLength); - // Steps 4-16. - return ::BalanceTimeDurationSlow(cx, nanos, largestUnit, result); + return {days, time, dayLength}; } /** - * CreateDateDurationRecord ( years, months, weeks, days ) + * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) */ -static DateDuration CreateDateDurationRecord(double years, double months, - double weeks, double days) { - MOZ_ASSERT(IsValidDuration({years, months, weeks, days})); - return {years, months, weeks, days}; +static TimeDuration CreateTimeDurationRecord(int64_t days, int64_t hours, + int64_t minutes, int64_t seconds, + int64_t milliseconds, + int64_t microseconds, + int64_t nanoseconds) { + // Step 1. + MOZ_ASSERT(IsValidDuration( + {0, 0, 0, double(days), double(hours), double(minutes), double(seconds), + double(milliseconds), double(microseconds), double(nanoseconds)})); + + // All values are safe integers, so we don't need to convert to `double` and + // back for the `ℝ(𝔽(x))` conversion. + MOZ_ASSERT(IsSafeInteger(days)); + MOZ_ASSERT(IsSafeInteger(hours)); + MOZ_ASSERT(IsSafeInteger(minutes)); + MOZ_ASSERT(IsSafeInteger(seconds)); + MOZ_ASSERT(IsSafeInteger(milliseconds)); + MOZ_ASSERT(IsSafeInteger(microseconds)); + MOZ_ASSERT(IsSafeInteger(nanoseconds)); + + // Step 2. + return { + days, + hours, + minutes, + seconds, + milliseconds, + double(microseconds), + double(nanoseconds), + }; } /** - * CreateDateDurationRecord ( years, months, weeks, days ) + * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) */ -static bool CreateDateDurationRecord(JSContext* cx, double years, double months, - double weeks, double days, - DateDuration* result) { - if (!ThrowIfInvalidDuration(cx, {years, months, weeks, days})) { - return false; - } - - *result = {years, months, weeks, days}; - return true; -} - -static bool UnbalanceDateDurationRelativeHasEffect(const Duration& duration, - TemporalUnit largestUnit) { - MOZ_ASSERT(largestUnit != TemporalUnit::Auto); +static TimeDuration CreateTimeDurationRecord(int64_t milliseconds, + const Int128& microseconds, + const Int128& nanoseconds) { + // Step 1. + MOZ_ASSERT(IsValidDuration({0, 0, 0, 0, 0, 0, 0, double(milliseconds), + double(microseconds), double(nanoseconds)})); - // Steps 2, 3.a-b, 4.a-b, 6-7. - return (largestUnit > TemporalUnit::Year && duration.years != 0) || - (largestUnit > TemporalUnit::Month && duration.months != 0) || - (largestUnit > TemporalUnit::Week && duration.weeks != 0); + // Step 2. + return { + 0, 0, 0, 0, milliseconds, double(microseconds), double(nanoseconds), + }; } /** - * UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, - * plainRelativeTo, calendarRec ) + * BalanceTimeDuration ( norm, largestUnit ) */ -static bool UnbalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - Handle> plainRelativeTo, - Handle calendar, DateDuration* result) { - MOZ_ASSERT(IsValidDuration(duration)); +TimeDuration js::temporal::BalanceTimeDuration( + const NormalizedTimeDuration& duration, TemporalUnit largestUnit) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(largestUnit <= TemporalUnit::Second, + "fallible fractional seconds units"); - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + auto [seconds, nanoseconds] = duration; - // Step 1. (Not applicable in our implementation.) - - // Steps 2, 3.a, 4.a, and 6. - if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { - // Steps 2.a, 3.a, 4.a, and 6. - *result = CreateDateDurationRecord(years, months, weeks, days); - return true; + // Negative nanoseconds are represented as the difference to 1'000'000'000. + // Convert these back to their absolute value and adjust the seconds part + // accordingly. + // + // For example the nanoseconds duration |-1n| is represented as the + // duration {seconds: -1, nanoseconds: 999'999'999}. + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= ToNanoseconds(TemporalUnit::Second); } - // Step 3. - if (largestUnit == TemporalUnit::Month) { - // Step 3.a. (Handled above) - MOZ_ASSERT(years != 0); - - // Step 3.b. (Not applicable in our implementation.) + // Step 1. + int64_t days = 0; + int64_t hours = 0; + int64_t minutes = 0; + int64_t milliseconds = 0; + int64_t microseconds = 0; - // Step 3.c. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + // Steps 2-3. (Not applicable in our implementation.) + // + // We don't need to convert to positive numbers, because integer division + // truncates and the %-operator has modulo semantics. - // Step 3.d. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + // Steps 4-10. + switch (largestUnit) { + // Step 4. + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Day: { + // Step 4.a. + microseconds = nanoseconds / 1000; - // Step 3.e. - auto yearsDuration = Duration{years}; + // Step 4.b. + nanoseconds = nanoseconds % 1000; - // Step 3.f. - Rooted> later( - cx, CalendarDateAdd(cx, calendar, plainRelativeTo, yearsDuration)); - if (!later) { - return false; - } + // Step 4.c. + milliseconds = microseconds / 1000; - // Steps 3.g-i. - Duration untilResult; - if (!CalendarDateUntil(cx, calendar, plainRelativeTo, later, - TemporalUnit::Month, &untilResult)) { - return false; - } + // Step 4.d. + microseconds = microseconds % 1000; - // Step 3.j. - double yearsInMonths = untilResult.months; + // Steps 4.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 3.k. - // - // The addition |months + yearsInMonths| can be imprecise, but this is - // safe to ignore, because all values are passed to - // CreateDateDurationRecord, which converts the values to Numbers. - return CreateDateDurationRecord(cx, 0, months + yearsInMonths, weeks, days, - result); - } + // Step 4.g. + minutes = seconds / 60; - // Step 4. - if (largestUnit == TemporalUnit::Week) { - // Step 4.a. (Handled above) - MOZ_ASSERT(years != 0 || months != 0); + // Step 4.h. + seconds = seconds % 60; - // Step 4.b. (Not applicable in our implementation.) + // Step 4.i. + hours = minutes / 60; - // Step 4.c. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + // Step 4.j. + minutes = minutes % 60; - // Step 4.d. - auto yearsMonthsDuration = Duration{years, months}; + // Step 4.k. + days = hours / 24; - // Step 4.e. - auto later = - CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsDuration); - if (!later) { - return false; - } - auto laterDate = ToPlainDate(&later.unwrap()); + // Step 4.l. + hours = hours % 24; - auto* unwrappedRelativeTo = plainRelativeTo.unwrap(cx); - if (!unwrappedRelativeTo) { - return false; + break; } - auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - - // Step 4.f. - int32_t yearsMonthsInDays = DaysUntil(relativeToDate, laterDate); - - // Step 4.g. - // - // The addition |days + yearsMonthsInDays| can be imprecise, but this is - // safe to ignore, because all values are passed to - // CreateDateDurationRecord, which converts the values to Numbers. - return CreateDateDurationRecord(cx, 0, 0, weeks, days + yearsMonthsInDays, - result); - } - - // Step 5. (Not applicable in our implementation.) - - // Step 6. (Handled above) - MOZ_ASSERT(years != 0 || months != 0 || weeks != 0); - - // FIXME: why don't we unconditionally throw an error for missing calendars? - - // Step 7. (Not applicable in our implementation.) - - // Step 8. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); - - // Step 9. - auto yearsMonthsWeeksDuration = Duration{years, months, weeks}; - - // Step 10. - auto later = - CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsWeeksDuration); - if (!later) { - return false; - } - auto laterDate = ToPlainDate(&later.unwrap()); - - auto* unwrappedRelativeTo = plainRelativeTo.unwrap(cx); - if (!unwrappedRelativeTo) { - return false; - } - auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 11. - int32_t yearsMonthsWeeksInDay = DaysUntil(relativeToDate, laterDate); + // Step 5. + case TemporalUnit::Hour: { + // Step 5.a. + microseconds = nanoseconds / 1000; - // Step 12. - // - // The addition |days + yearsMonthsWeeksInDay| can be imprecise, but this is - // safe to ignore, because all values are passed to CreateDateDurationRecord, - // which converts the values to Numbers. - return CreateDateDurationRecord(cx, 0, 0, 0, days + yearsMonthsWeeksInDay, - result); -} + // Step 5.b. + nanoseconds = nanoseconds % 1000; -/** - * UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, - * plainRelativeTo, calendarRec ) - */ -static bool UnbalanceDateDurationRelative(JSContext* cx, - const Duration& duration, - TemporalUnit largestUnit, - DateDuration* result) { - MOZ_ASSERT(IsValidDuration(duration)); + // Step 5.c. + milliseconds = microseconds / 1000; - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + // Step 5.d. + microseconds = microseconds % 1000; - // Step 1. (Not applicable.) + // Steps 5.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Steps 2, 3.a, 4.a, and 6. - if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { - // Steps 2.a, 3.a, 4.a, and 6. - *result = CreateDateDurationRecord(years, months, weeks, days); - return true; - } + // Step 5.g. + minutes = seconds / 60; - // Step 5. (Not applicable.) + // Step 5.h. + seconds = seconds % 60; - // Steps 3.b, 4.b, and 7. - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, "calendar"); - return false; -} + // Step 5.i. + hours = minutes / 60; -/** - * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, - * smallestUnit, plainRelativeTo, calendarRec ) - */ -static bool BalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - TemporalUnit smallestUnit, - Handle> plainRelativeTo, - Handle calendar, DateDuration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(largestUnit <= smallestUnit); + // Step 5.j. + minutes = minutes % 60; - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + break; + } - // FIXME: spec issue - effectful code paths should be more fine-grained - // similar to UnbalanceDateDurationRelative. For example: - // 1. If largestUnit = "year" and days = 0 and months = 0, then no-op. - // 2. Else if largestUnit = "month" and days = 0, then no-op. - // 3. Else if days = 0, then no-op. - // - // Also note that |weeks| is never balanced, even when non-zero. + case TemporalUnit::Minute: { + // Step 6.a. + microseconds = nanoseconds / 1000; - // Step 1. (Not applicable in our implementation.) + // Step 6.b. + nanoseconds = nanoseconds % 1000; - // Steps 2-4. - if (largestUnit > TemporalUnit::Week || - (years == 0 && months == 0 && weeks == 0 && days == 0)) { - // Step 4.a. - *result = CreateDateDurationRecord(years, months, weeks, days); - return true; - } + // Step 6.c. + milliseconds = microseconds / 1000; - // Step 5. - if (!plainRelativeTo) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, - "relativeTo"); - return false; - } + // Step 6.d. + microseconds = microseconds % 1000; - // Step 6. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + // Steps 6.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 7. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + // Step 6.g. + minutes = seconds / 60; - // Steps 8-9. (Not applicable in our implementation.) + // Step 6.h. + seconds = seconds % 60; - auto untilAddedDate = [&](const Duration& duration, Duration* untilResult) { - Rooted> later( - cx, AddDate(cx, calendar, plainRelativeTo, duration)); - if (!later) { - return false; + break; } - return CalendarDateUntil(cx, calendar, plainRelativeTo, later, largestUnit, - untilResult); - }; + // Step 7. + case TemporalUnit::Second: { + // Step 7.a. + microseconds = nanoseconds / 1000; - // Step 10. - if (largestUnit == TemporalUnit::Year) { - // Step 10.a. - if (smallestUnit == TemporalUnit::Week) { - // Step 10.a.i. - MOZ_ASSERT(days == 0); + // Step 7.b. + nanoseconds = nanoseconds % 1000; - // Step 10.a.ii. - auto yearsMonthsDuration = Duration{years, months}; + // Step 7.c. + milliseconds = microseconds / 1000; - // Steps 10.a.iii-iv. - Duration untilResult; - if (!untilAddedDate(yearsMonthsDuration, &untilResult)) { - return false; - } + // Step 7.d. + microseconds = microseconds % 1000; - // FIXME: spec bug - CreateDateDurationRecord is infallible + // Steps 7.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 10.a.v. - *result = CreateDateDurationRecord(untilResult.years, untilResult.months, - weeks, 0); - return true; + break; } - // Step 10.b. - auto yearsMonthsWeeksDaysDuration = Duration{years, months, weeks, days}; + case TemporalUnit::Millisecond: + case TemporalUnit::Microsecond: + case TemporalUnit::Nanosecond: + case TemporalUnit::Auto: + MOZ_CRASH("Unexpected temporal unit"); + } - // Steps 10.c-d. - Duration untilResult; - if (!untilAddedDate(yearsMonthsWeeksDaysDuration, &untilResult)) { - return false; - } + // Step 11. + return CreateTimeDurationRecord(days, hours, minutes, seconds, milliseconds, + microseconds, nanoseconds); +} - // FIXME: spec bug - CreateDateDurationRecord is infallible - // https://github.com/tc39/proposal-temporal/issues/2750 +/** + * BalanceTimeDuration ( norm, largestUnit ) + */ +bool js::temporal::BalanceTimeDuration(JSContext* cx, + const NormalizedTimeDuration& duration, + TemporalUnit largestUnit, + TimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); - // Step 10.e. - *result = CreateDateDurationRecord(untilResult.years, untilResult.months, - untilResult.weeks, untilResult.days); - return true; - } + auto [seconds, nanoseconds] = duration; - // Step 11. - if (largestUnit == TemporalUnit::Month) { - // Step 11.a. - MOZ_ASSERT(years == 0); + // Negative nanoseconds are represented as the difference to 1'000'000'000. + // Convert these back to their absolute value and adjust the seconds part + // accordingly. + // + // For example the nanoseconds duration |-1n| is represented as the + // duration {seconds: -1, nanoseconds: 999'999'999}. + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= ToNanoseconds(TemporalUnit::Second); + } - // Step 11.b. - if (smallestUnit == TemporalUnit::Week) { - // Step 10.b.i. - MOZ_ASSERT(days == 0); + // Steps 1-3. (Not applicable in our implementation.) + // + // We don't need to convert to positive numbers, because integer division + // truncates and the %-operator has modulo semantics. - // Step 10.b.ii. - *result = CreateDateDurationRecord(0, months, weeks, 0); + // Steps 4-10. + switch (largestUnit) { + // Steps 4-7. + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Day: + case TemporalUnit::Hour: + case TemporalUnit::Minute: + case TemporalUnit::Second: + *result = BalanceTimeDuration(duration, largestUnit); return true; - } - // Step 11.c. - auto monthsWeeksDaysDuration = Duration{0, months, weeks, days}; + // Step 8. + case TemporalUnit::Millisecond: { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = + (int64_t(1) << 53) * ToMilliseconds(TemporalUnit::Second); - // Steps 11.d-e. - Duration untilResult; - if (!untilAddedDate(monthsWeeksDaysDuration, &untilResult)) { - return false; - } + // The largest possible milliseconds value whose double representation + // doesn't exceed the normalized seconds limit. + constexpr auto max = int64_t(0x7cff'ffff'ffff'fdff); - // FIXME: spec bug - CreateDateDurationRecord is infallible - // https://github.com/tc39/proposal-temporal/issues/2750 + // Assert |max| is the maximum allowed milliseconds value. + static_assert(double(max) < double(limit)); + static_assert(double(max + 1) >= double(limit)); - // Step 11.f. - *result = CreateDateDurationRecord(0, untilResult.months, untilResult.weeks, - untilResult.days); - return true; - } + static_assert((NormalizedTimeDuration::max().seconds + 1) * + ToMilliseconds(TemporalUnit::Second) <= + INT64_MAX, + "total number duration milliseconds fits into int64"); - // Step 12. - MOZ_ASSERT(largestUnit == TemporalUnit::Week); + // Step 8.a. + int64_t microseconds = nanoseconds / 1000; - // Step 13. - MOZ_ASSERT(years == 0); + // Step 8.b. + nanoseconds = nanoseconds % 1000; - // Step 14. - MOZ_ASSERT(months == 0); + // Step 8.c. + int64_t milliseconds = microseconds / 1000; + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 15. - auto weeksDaysDuration = Duration{0, 0, weeks, days}; + // Step 8.d. + microseconds = microseconds % 1000; - // Steps 16-17. - Duration untilResult; - if (!untilAddedDate(weeksDaysDuration, &untilResult)) { - return false; - } + auto millis = + (seconds * ToMilliseconds(TemporalUnit::Second)) + milliseconds; + if (std::abs(millis) > max) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } - // FIXME: spec bug - CreateDateDurationRecord is infallible - // https://github.com/tc39/proposal-temporal/issues/2750 + // Step 11. + *result = CreateTimeDurationRecord(millis, Int128{microseconds}, + Int128{nanoseconds}); + return true; + } - // Step 18. - *result = CreateDateDurationRecord(0, 0, untilResult.weeks, untilResult.days); - return true; -} + // Step 9. + case TemporalUnit::Microsecond: { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = Uint128{int64_t(1) << 53} * + Uint128{ToMicroseconds(TemporalUnit::Second)}; -/** - * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, - * smallestUnit, plainRelativeTo, calendarRec ) - */ -bool js::temporal::BalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - TemporalUnit smallestUnit, - Handle> plainRelativeTo, - Handle calendar, DateDuration* result) { - MOZ_ASSERT(plainRelativeTo); - MOZ_ASSERT(calendar.receiver()); + // The largest possible microseconds value whose double representation + // doesn't exceed the normalized seconds limit. + constexpr auto max = + (Uint128{0x1e8} << 64) + Uint128{0x47ff'ffff'fff7'ffff}; + static_assert(max < limit); - return ::BalanceDateDurationRelative(cx, duration, largestUnit, smallestUnit, - plainRelativeTo, calendar, result); -} + // Assert |max| is the maximum allowed microseconds value. + MOZ_ASSERT(double(max) < double(limit)); + MOZ_ASSERT(double(max + Uint128{1}) >= double(limit)); -/** - * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, - * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, - * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) - */ -static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, - Duration* duration) { - MOZ_ASSERT(IsValidDuration(one)); - MOZ_ASSERT(IsValidDuration(two)); + // Step 9.a. + int64_t microseconds = nanoseconds / 1000; + MOZ_ASSERT(std::abs(microseconds) <= 999'999); - // Steps 1-2. (Not applicable) + // Step 9.b. + nanoseconds = nanoseconds % 1000; - // Step 3. - auto largestUnit1 = DefaultTemporalLargestUnit(one); + auto micros = + (Int128{seconds} * Int128{ToMicroseconds(TemporalUnit::Second)}) + + Int128{microseconds}; + if (micros.abs() > max) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } - // Step 4. - auto largestUnit2 = DefaultTemporalLargestUnit(two); + // Step 11. + *result = CreateTimeDurationRecord(0, micros, Int128{nanoseconds}); + return true; + } - // Step 5. - auto largestUnit = std::min(largestUnit1, largestUnit2); + // Step 10. + case TemporalUnit::Nanosecond: { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = Uint128{int64_t(1) << 53} * + Uint128{ToNanoseconds(TemporalUnit::Second)}; + + // The largest possible nanoseconds value whose double representation + // doesn't exceed the normalized seconds limit. + constexpr auto max = + (Uint128{0x77359} << 64) + Uint128{0x3fff'ffff'dfff'ffff}; + static_assert(max < limit); + + // Assert |max| is the maximum allowed nanoseconds value. + MOZ_ASSERT(double(max) < double(limit)); + MOZ_ASSERT(double(max + Uint128{1}) >= double(limit)); + + MOZ_ASSERT(std::abs(nanoseconds) <= 999'999'999); + + auto nanos = + (Int128{seconds} * Int128{ToNanoseconds(TemporalUnit::Second)}) + + Int128{nanoseconds}; + if (nanos.abs() > max) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } - // Step 6.a. - if (largestUnit <= TemporalUnit::Week) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, - "relativeTo"); - return false; - } + // Step 11. + *result = CreateTimeDurationRecord(0, Int128{}, nanos); + return true; + } - // Step 6.b. - TimeDuration result; - if (!BalanceTimeDuration(cx, one, two, largestUnit, &result)) { - return false; + case TemporalUnit::Auto: + break; } - - // Steps 6.c. - *duration = result.toDuration(); - return true; + MOZ_CRASH("Unexpected temporal unit"); } /** - * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, - * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, - * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + * BalanceTimeDurationRelative ( days, norm, largestUnit, zonedRelativeTo, + * timeZoneRec, precalculatedPlainDateTime ) */ -static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, - Handle> plainRelativeTo, - Handle calendar, Duration* duration) { - MOZ_ASSERT(IsValidDuration(one)); - MOZ_ASSERT(IsValidDuration(two)); +static bool BalanceTimeDurationRelative( + JSContext* cx, const NormalizedDuration& duration, TemporalUnit largestUnit, + Handle relativeTo, Handle timeZone, + mozilla::Maybe precalculatedPlainDateTime, + TimeDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); - // Steps 1-2. (Not applicable) + // Step 1. + const auto& startNs = relativeTo.instant(); - // FIXME: spec issue - calendarRec is not undefined when plainRelativeTo is - // not undefined. + // Step 2. + const auto& startInstant = startNs; // Step 3. - auto largestUnit1 = DefaultTemporalLargestUnit(one); + auto intermediateNs = startNs; // Step 4. - auto largestUnit2 = DefaultTemporalLargestUnit(two); + PlainDateTime startDateTime; + if (duration.date.days != 0) { + // Step 4.a. + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { + return false; + } + precalculatedPlainDateTime = + mozilla::SomeRef(startDateTime); + } - // Step 5. - auto largestUnit = std::min(largestUnit1, largestUnit2); + // Steps 4.b-c. + Rooted isoCalendar(cx, CalendarValue(cx->names().iso8601)); + if (!AddDaysToZonedDateTime(cx, startInstant, *precalculatedPlainDateTime, + timeZone, isoCalendar, duration.date.days, + &intermediateNs)) { + return false; + } + } - // Step 6. (Not applicable) + // Step 5. + Instant endNs; + if (!AddInstant(cx, intermediateNs, duration.time, &endNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(endNs)); - // Step 7.a. (Not applicable in our implementation.) + // Step 6. + auto normalized = + NormalizedTimeDurationFromEpochNanosecondsDifference(endNs, startInstant); - // Step 7.b. - auto dateDuration1 = one.date(); + // Step 7. + if (normalized == NormalizedTimeDuration{}) { + *result = {}; + return true; + } - // Step 7.c. - auto dateDuration2 = two.date(); + // Steps 8-9. + int64_t days = 0; + if (TemporalUnit::Year <= largestUnit && largestUnit <= TemporalUnit::Day) { + // Step 8.a. + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { + return false; + } + precalculatedPlainDateTime = + mozilla::SomeRef(startDateTime); + } - // FIXME: spec issue - calendarUnitsPresent is unused. + // Step 8.b. + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, normalized, relativeTo, timeZone, + *precalculatedPlainDateTime, + &timeAndDays)) { + return false; + } - // Step 7.d. - [[maybe_unused]] bool calendarUnitsPresent = true; + // Step 8.c. + days = timeAndDays.days; - // Step 7.e. - if (dateDuration1.years == 0 && dateDuration1.months == 0 && - dateDuration1.weeks == 0 && dateDuration2.years == 0 && - dateDuration2.months == 0 && dateDuration2.weeks == 0) { - calendarUnitsPresent = false; - } + // Step 8.d. + normalized = NormalizedTimeDuration::fromNanoseconds(timeAndDays.time); + MOZ_ASSERT_IF(days > 0, normalized >= NormalizedTimeDuration{}); + MOZ_ASSERT_IF(days < 0, normalized <= NormalizedTimeDuration{}); - // Step 7.f. - Rooted> intermediate( - cx, AddDate(cx, calendar, plainRelativeTo, dateDuration1)); - if (!intermediate) { - return false; + // Step 8.e. + largestUnit = TemporalUnit::Hour; } - // Step 7.g. - Rooted> end( - cx, AddDate(cx, calendar, intermediate, dateDuration2)); - if (!end) { + // Step 10. + TimeDuration balanceResult; + if (!BalanceTimeDuration(cx, normalized, largestUnit, &balanceResult)) { return false; } - // Step 7.h. - auto dateLargestUnit = std::min(TemporalUnit::Day, largestUnit); + // Step 11. + *result = { + days, + balanceResult.hours, + balanceResult.minutes, + balanceResult.seconds, + balanceResult.milliseconds, + balanceResult.microseconds, + balanceResult.nanoseconds, + }; + MOZ_ASSERT(IsValidDuration(result->toDuration())); + return true; +} - // Steps 7.i-k. - Duration dateDifference; - if (!DifferenceDate(cx, calendar, plainRelativeTo, end, dateLargestUnit, - &dateDifference)) { - return false; - } +/** + * CreateDateDurationRecord ( years, months, weeks, days ) + */ +static DateDuration CreateDateDurationRecord(int64_t years, int64_t months, + int64_t weeks, int64_t days) { + MOZ_ASSERT(IsValidDuration(Duration{ + double(years), + double(months), + double(weeks), + double(days), + })); + return {years, months, weeks, days}; +} - // Step 7.l. - TimeDuration result; - if (!BalanceTimeDuration(cx, dateDifference.days, one.time(), two.time(), - largestUnit, &result)) { +/** + * CreateDateDurationRecord ( years, months, weeks, days ) + */ +static bool CreateDateDurationRecord(JSContext* cx, int64_t years, + int64_t months, int64_t weeks, + int64_t days, DateDuration* result) { + auto duration = DateDuration{years, months, weeks, days}; + if (!ThrowIfInvalidDuration(cx, duration)) { return false; } - // Steps 7.m. - *duration = { - dateDifference.years, dateDifference.months, dateDifference.weeks, - result.days, result.hours, result.minutes, - result.seconds, result.milliseconds, result.microseconds, - result.nanoseconds, - }; + *result = duration; return true; } +static bool UnbalanceDateDurationRelativeHasEffect(const DateDuration& duration, + TemporalUnit largestUnit) { + MOZ_ASSERT(largestUnit != TemporalUnit::Auto); + + // Steps 2-4. + return (largestUnit > TemporalUnit::Year && duration.years != 0) || + (largestUnit > TemporalUnit::Month && duration.months != 0) || + (largestUnit > TemporalUnit::Week && duration.weeks != 0); +} + /** - * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, - * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, - * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + * UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * plainRelativeTo, calendarRec ) */ -static bool AddDuration( - JSContext* cx, const Duration& one, const Duration& two, - Handle zonedRelativeTo, Handle calendar, - Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - Duration* result) { - // Steps 1-2. (Not applicable) +static bool UnbalanceDateDurationRelative( + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, + Handle> plainRelativeTo, + Handle calendar, DateDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); - // Step 3. - auto largestUnit1 = DefaultTemporalLargestUnit(one); + auto [years, months, weeks, days] = duration; - // Step 4. - auto largestUnit2 = DefaultTemporalLargestUnit(two); + // Step 1. (Not applicable in our implementation.) + + // Steps 2-4. + if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { + *result = duration; + return true; + } // Step 5. - auto largestUnit = std::min(largestUnit1, largestUnit2); + MOZ_ASSERT(largestUnit != TemporalUnit::Year); - // Steps 6-7. (Not applicable) + // Step 6. (Not applicable in our implementation.) - // Steps 8-9. (Not applicable in our implementation.) + // Step 7. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); - // FIXME: spec issue - GetPlainDateTimeFor called unnecessarily - // - // clang-format off - // - // 10. If largestUnit is one of "year", "month", "week", or "day", then - // a. If precalculatedPlainDateTime is undefined, then - // i. Let startDateTime be ? GetPlainDateTimeFor(timeZone, zonedRelativeTo.[[Nanoseconds]], calendar). - // b. Else, - // i. Let startDateTime be precalculatedPlainDateTime. - // c. Let intermediateNs be ? AddZonedDateTime(zonedRelativeTo.[[Nanoseconds]], timeZone, calendar, y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, startDateTime). - // d. Let endNs be ? AddZonedDateTime(intermediateNs, timeZone, calendar, y2, mon2, w2, d2, h2, min2, s2, ms2, mus2, ns2). - // e. Return ? DifferenceZonedDateTime(zonedRelativeTo.[[Nanoseconds]], endNs, timeZone, calendar, largestUnit, OrdinaryObjectCreate(null), startDateTime). - // 11. Let intermediateNs be ? AddInstant(zonedRelativeTo.[[Nanoseconds]], h1, min1, s1, ms1, mus1, ns1). - // 12. Let endNs be ? AddInstant(intermediateNs, h2, min2, s2, ms2, mus2, ns2). - // 13. Let result be DifferenceInstant(zonedRelativeTo.[[Nanoseconds]], endNs, 1, "nanosecond", largestUnit, "halfExpand"). - // 14. Return ! CreateDurationRecord(0, 0, 0, 0, result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). - // - // clang-format on + // Step 8. + if (largestUnit == TemporalUnit::Month) { + // Step 8.a. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); - // Step 10. - bool startDateTimeNeeded = largestUnit <= TemporalUnit::Day; + // Step 8.b. + auto yearsDuration = DateDuration{years}; - // Steps 11-14 and 16. - if (startDateTimeNeeded) { - // Steps 11-12. - PlainDateTime startDateTime; - if (!precalculatedPlainDateTime) { - if (!GetPlainDateTimeFor(cx, timeZone, zonedRelativeTo.instant(), - &startDateTime)) { - return false; - } - } else { - startDateTime = *precalculatedPlainDateTime; + // Step 8.c. + Rooted> later( + cx, CalendarDateAdd(cx, calendar, plainRelativeTo, yearsDuration)); + if (!later) { + return false; } - // Step 13. - Instant intermediateNs; - if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, - one, startDateTime, &intermediateNs)) { + // Steps 8.d-f. + DateDuration untilResult; + if (!CalendarDateUntil(cx, calendar, plainRelativeTo, later, + TemporalUnit::Month, &untilResult)) { return false; } - MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); - // Step 14. - Instant endNs; - if (!AddZonedDateTime(cx, intermediateNs, timeZone, calendar, two, - &endNs)) { + // Step 8.g. + int64_t yearsInMonths = untilResult.months; + + // Step 8.h. + return CreateDateDurationRecord(cx, 0, months + yearsInMonths, weeks, days, + result); + } + + // Step 9. + if (largestUnit == TemporalUnit::Week) { + // Step 9.a. + auto yearsMonthsDuration = DateDuration{years, months}; + + // Step 9.b. + auto later = + CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsDuration); + if (!later) { return false; } - MOZ_ASSERT(IsValidEpochInstant(endNs)); + auto laterDate = ToPlainDate(&later.unwrap()); - // Step 15. (Not applicable) + auto* unwrappedRelativeTo = plainRelativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { + return false; + } + auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 16. - return DifferenceZonedDateTime(cx, zonedRelativeTo.instant(), endNs, - timeZone, calendar, largestUnit, - startDateTime, result); + // Step 9.c. + int32_t yearsMonthsInDays = DaysUntil(relativeToDate, laterDate); + + // Step 9.d. + return CreateDateDurationRecord(cx, 0, 0, weeks, days + yearsMonthsInDays, + result); } - // Steps 11-12. (Not applicable) + // Step 10. (Not applicable in our implementation.) - // Step 13. (Inlined AddZonedDateTime, step 6.) - Instant intermediateNs; - if (!AddInstant(cx, zonedRelativeTo.instant(), one, &intermediateNs)) { + // Step 11. + auto yearsMonthsWeeksDuration = DateDuration{years, months, weeks}; + + // Step 12. + auto later = + CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsWeeksDuration); + if (!later) { return false; } - MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); + auto laterDate = ToPlainDate(&later.unwrap()); - // Step 14. (Inlined AddZonedDateTime, step 6.) - Instant endNs; - if (!AddInstant(cx, intermediateNs, two, &endNs)) { + auto* unwrappedRelativeTo = plainRelativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { return false; } - MOZ_ASSERT(IsValidEpochInstant(endNs)); + auto relativeToDate = ToPlainDate(unwrappedRelativeTo); + + // Step 13. + int32_t yearsMonthsWeeksInDay = DaysUntil(relativeToDate, laterDate); - // Steps 15.a-b. - return DifferenceInstant(cx, zonedRelativeTo.instant(), endNs, Increment{1}, - TemporalUnit::Nanosecond, largestUnit, - TemporalRoundingMode::HalfExpand, result); + // Step 14. + return CreateDateDurationRecord(cx, 0, 0, 0, days + yearsMonthsWeeksInDay, + result); } /** - * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, - * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, - * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + * UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * plainRelativeTo, calendarRec ) */ -static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, - Handle zonedRelativeTo, - Handle calendar, - Handle timeZone, Duration* result) { - return AddDuration(cx, one, two, zonedRelativeTo, calendar, timeZone, - mozilla::Nothing(), result); -} - -static bool RoundDuration(JSContext* cx, int64_t totalNanoseconds, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, Duration* result) { - MOZ_ASSERT(unit >= TemporalUnit::Hour); - - double rounded; - if (!RoundNumberToIncrement(cx, totalNanoseconds, unit, increment, - roundingMode, &rounded)) { - return false; - } - - double hours = 0; - double minutes = 0; - double seconds = 0; - double milliseconds = 0; - double microseconds = 0; - double nanoseconds = 0; - - switch (unit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Week: - case TemporalUnit::Month: - case TemporalUnit::Day: - MOZ_CRASH("Unexpected temporal unit"); - - case TemporalUnit::Hour: - hours = rounded; - break; - case TemporalUnit::Minute: - minutes = rounded; - break; - case TemporalUnit::Second: - seconds = rounded; - break; - case TemporalUnit::Millisecond: - milliseconds = rounded; - break; - case TemporalUnit::Microsecond: - microseconds = rounded; - break; - case TemporalUnit::Nanosecond: - nanoseconds = rounded; - break; - } - - *result = { - 0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, - nanoseconds, - }; - return ThrowIfInvalidDuration(cx, *result); -} +static bool UnbalanceDateDurationRelative(JSContext* cx, + const DateDuration& duration, + TemporalUnit largestUnit, + DateDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); -static bool RoundDuration(JSContext* cx, Handle totalNanoseconds, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, Duration* result) { - MOZ_ASSERT(unit >= TemporalUnit::Hour); + // Step 1. (Not applicable.) - double rounded; - if (!RoundNumberToIncrement(cx, totalNanoseconds, unit, increment, - roundingMode, &rounded)) { - return false; + // Step 2-4. + if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { + *result = duration; + return true; } - double hours = 0; - double minutes = 0; - double seconds = 0; - double milliseconds = 0; - double microseconds = 0; - double nanoseconds = 0; - - switch (unit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Week: - case TemporalUnit::Month: - case TemporalUnit::Day: - MOZ_CRASH("Unexpected temporal unit"); - - case TemporalUnit::Hour: - hours = rounded; - break; - case TemporalUnit::Minute: - minutes = rounded; - break; - case TemporalUnit::Second: - seconds = rounded; - break; - case TemporalUnit::Millisecond: - milliseconds = rounded; - break; - case TemporalUnit::Microsecond: - microseconds = rounded; - break; - case TemporalUnit::Nanosecond: - nanoseconds = rounded; - break; - } + // Step 5. + MOZ_ASSERT(largestUnit != TemporalUnit::Year); - *result = { - 0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, - nanoseconds, - }; - return ThrowIfInvalidDuration(cx, *result); + // Steps 6. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, "calendar"); + return false; } /** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, - * precalculatedPlainDateTime ) + * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * smallestUnit, plainRelativeTo, calendarRec ) */ -static bool AdjustRoundedDurationDaysSlow( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle zonedRelativeTo, Handle calendar, - Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - InstantSpan dayLength, Duration* result) { +static bool BalanceDateDurationRelative( + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, + TemporalUnit smallestUnit, + Handle> plainRelativeTo, + Handle calendar, DateDuration* result) { MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(IsValidInstantSpan(dayLength)); - - // Step 3. - Rooted timeRemainderNs( - cx, TotalDurationNanosecondsSlow(cx, duration.time())); - if (!timeRemainderNs) { - return false; - } - - // Steps 4-6. - int32_t direction = timeRemainderNs->sign(); + MOZ_ASSERT(largestUnit <= smallestUnit); - // Steps 7-10. (Computed in caller) + auto [years, months, weeks, days] = duration; - // Step 11. - Rooted dayLengthNs(cx, ToEpochNanoseconds(cx, dayLength)); - if (!dayLengthNs) { - return false; - } - MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + // FIXME: spec issue - effectful code paths should be more fine-grained + // similar to UnbalanceDateDurationRelative. For example: + // 1. If largestUnit = "year" and days = 0 and months = 0, then no-op. + // 2. Else if largestUnit = "month" and days = 0, then no-op. + // 3. Else if days = 0, then no-op. + // + // Also note that |weeks| is never balanced, even when non-zero. - // Step 12. - Rooted oneDayLess(cx, BigInt::sub(cx, timeRemainderNs, dayLengthNs)); - if (!oneDayLess) { - return false; - } + // Step 1. (Not applicable in our implementation.) - // Step 13. - if ((direction > 0 && oneDayLess->sign() < 0) || - (direction < 0 && oneDayLess->sign() > 0)) { + // Steps 2-4. + if (largestUnit > TemporalUnit::Week || + (years == 0 && months == 0 && weeks == 0 && days == 0)) { + // Step 4.a. *result = duration; return true; } - // Step 14. - Duration adjustedDateDuration; - if (!AddDuration(cx, - { - duration.years, - duration.months, - duration.weeks, - duration.days, - }, - {0, 0, 0, double(direction)}, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, - &adjustedDateDuration)) { + // Step 5. + if (!plainRelativeTo) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, + "relativeTo"); return false; } - // Step 15. - Duration roundedTimeDuration; - if (!RoundDuration(cx, oneDayLess, unit, increment, roundingMode, - &roundedTimeDuration)) { - return false; - } + // Step 6. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); - // Step 16. - TimeDuration adjustedTimeDuration; - if (!BalanceTimeDuration(cx, roundedTimeDuration, TemporalUnit::Hour, - &adjustedTimeDuration)) { - return false; - } + // Step 7. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); - // Step 17. - *result = { - adjustedDateDuration.years, adjustedDateDuration.months, - adjustedDateDuration.weeks, adjustedDateDuration.days, - adjustedTimeDuration.hours, adjustedTimeDuration.minutes, - adjustedTimeDuration.seconds, adjustedTimeDuration.milliseconds, - adjustedTimeDuration.microseconds, adjustedTimeDuration.nanoseconds, - }; - MOZ_ASSERT(IsValidDuration(*result)); - return true; -} + // Steps 8-9. (Not applicable in our implementation.) -/** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, - * precalculatedPlainDateTime ) - */ -static bool AdjustRoundedDurationDays( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle zonedRelativeTo, Handle calendar, - Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); + auto untilAddedDate = [&](const DateDuration& duration, + DateDuration* untilResult) { + Rooted> later( + cx, AddDate(cx, calendar, plainRelativeTo, duration)); + if (!later) { + return false; + } - // Step 1. - if ((TemporalUnit::Year <= unit && unit <= TemporalUnit::Day) || - (unit == TemporalUnit::Nanosecond && increment == Increment{1})) { - *result = duration; - return true; - } + return CalendarDateUntil(cx, calendar, plainRelativeTo, later, largestUnit, + untilResult); + }; - // The increment is limited for all smaller temporal units. - MOZ_ASSERT(increment < MaximumTemporalDurationRoundingIncrement(unit)); + // Step 10. + if (largestUnit == TemporalUnit::Year) { + // Step 10.a. + if (smallestUnit == TemporalUnit::Week) { + // Step 10.a.i. + MOZ_ASSERT(days == 0); - // Step 2. - MOZ_ASSERT(precalculatedPlainDateTime); + // Step 10.a.ii. + auto yearsMonthsDuration = DateDuration{years, months}; - // Steps 4-6. - // - // Step 3 is moved below, so compute |direction| through DurationSign. - int32_t direction = DurationSign(duration.time()); + // Steps 10.a.iii-iv. + DateDuration untilResult; + if (!untilAddedDate(yearsMonthsDuration, &untilResult)) { + return false; + } - // Steps 7-8. - Instant dayStart; - if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, - duration.date(), *precalculatedPlainDateTime, - &dayStart)) { - return false; - } - MOZ_ASSERT(IsValidEpochInstant(dayStart)); + // Step 10.a.v. + *result = CreateDateDurationRecord(untilResult.years, untilResult.months, + weeks, 0); + return true; + } - // Step 9. - PlainDateTime dayStartDateTime; - if (!GetPlainDateTimeFor(cx, timeZone, dayStart, &dayStartDateTime)) { - return false; - } + // Step 10.b. + const auto& yearsMonthsWeeksDaysDuration = duration; - // Step 10. - Instant dayEnd; - if (!AddDaysToZonedDateTime(cx, dayStart, dayStartDateTime, timeZone, - zonedRelativeTo.calendar(), direction, &dayEnd)) { - return false; + // Steps 10.c-d. + DateDuration untilResult; + if (!untilAddedDate(yearsMonthsWeeksDaysDuration, &untilResult)) { + return false; + } + + // Step 10.e. + *result = CreateDateDurationRecord(untilResult.years, untilResult.months, + untilResult.weeks, untilResult.days); + return true; } - MOZ_ASSERT(IsValidEpochInstant(dayEnd)); // Step 11. - auto dayLength = dayEnd - dayStart; - MOZ_ASSERT(IsValidInstantSpan(dayLength)); + if (largestUnit == TemporalUnit::Month) { + // Step 11.a. + MOZ_ASSERT(years == 0); + + // Step 11.b. + if (smallestUnit == TemporalUnit::Week) { + // Step 10.b.i. + MOZ_ASSERT(days == 0); + + // Step 10.b.ii. + *result = CreateDateDurationRecord(0, months, weeks, 0); + return true; + } - // Step 3. (Reordered) - auto timeRemainderNs = TotalDurationNanoseconds(duration.time()); - if (!timeRemainderNs) { - return AdjustRoundedDurationDaysSlow( - cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, dayLength, result); + // Step 11.c. + const auto& monthsWeeksDaysDuration = duration; + + // Steps 11.d-e. + DateDuration untilResult; + if (!untilAddedDate(monthsWeeksDaysDuration, &untilResult)) { + return false; + } + + // Step 11.f. + *result = CreateDateDurationRecord(0, untilResult.months, untilResult.weeks, + untilResult.days); + return true; } // Step 12. - auto checkedOneDayLess = *timeRemainderNs - dayLength.toNanoseconds(); - if (!checkedOneDayLess.isValid()) { - return AdjustRoundedDurationDaysSlow( - cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, dayLength, result); - } - auto oneDayLess = checkedOneDayLess.value(); + MOZ_ASSERT(largestUnit == TemporalUnit::Week); // Step 13. - if ((direction > 0 && oneDayLess < 0) || (direction < 0 && oneDayLess > 0)) { - *result = duration; - return true; - } + MOZ_ASSERT(years == 0); // Step 14. - Duration adjustedDateDuration; - if (!AddDuration(cx, - { - duration.years, - duration.months, - duration.weeks, - duration.days, - }, - {0, 0, 0, double(direction)}, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, - &adjustedDateDuration)) { - return false; - } + MOZ_ASSERT(months == 0); // Step 15. - Duration roundedTimeDuration; - if (!RoundDuration(cx, oneDayLess, unit, increment, roundingMode, - &roundedTimeDuration)) { - return false; - } + const auto& weeksDaysDuration = duration; - // Step 16. - TimeDuration adjustedTimeDuration; - if (!BalanceTimeDuration(cx, roundedTimeDuration, TemporalUnit::Hour, - &adjustedTimeDuration)) { + // Steps 16-17. + DateDuration untilResult; + if (!untilAddedDate(weeksDaysDuration, &untilResult)) { return false; } - // FIXME: spec bug - CreateDurationRecord is fallible because the adjusted - // date and time durations can be have different signs. - // https://github.com/tc39/proposal-temporal/issues/2536 - // - // clang-format off - // - // { - // let calendar = new class extends Temporal.Calendar { - // dateAdd(date, duration, options) { - // console.log(`dateAdd(${date}, ${duration})`); - // if (duration.days === 10) { - // return super.dateAdd(date, duration.negated(), options); - // } - // return super.dateAdd(date, duration, options); - // } - // }("iso8601"); - // - // let zdt = new Temporal.ZonedDateTime(0n, "UTC", calendar); - // - // let d = Temporal.Duration.from({ - // days: 10, - // hours: 25, - // }); - // - // let r = d.round({ - // smallestUnit: "nanoseconds", - // roundingIncrement: 5, - // relativeTo: zdt, - // }); - // console.log(r.toString()); - // } - // - // clang-format on - - // Step 17. - *result = { - adjustedDateDuration.years, adjustedDateDuration.months, - adjustedDateDuration.weeks, adjustedDateDuration.days, - adjustedTimeDuration.hours, adjustedTimeDuration.minutes, - adjustedTimeDuration.seconds, adjustedTimeDuration.milliseconds, - adjustedTimeDuration.microseconds, adjustedTimeDuration.nanoseconds, - }; - return ThrowIfInvalidDuration(cx, *result); + // Step 18. + *result = CreateDateDurationRecord(0, 0, untilResult.weeks, untilResult.days); + return true; } /** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, - * precalculatedPlainDateTime ) + * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * smallestUnit, plainRelativeTo, calendarRec ) */ -bool js::temporal::AdjustRoundedDurationDays( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle zonedRelativeTo, Handle calendar, - Handle timeZone, - const PlainDateTime& precalculatedPlainDateTime, Duration* result) { - return ::AdjustRoundedDurationDays( - cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, - timeZone, mozilla::SomeRef(precalculatedPlainDateTime), result); -} - -static bool BigIntToStringBuilder(JSContext* cx, Handle num, - JSStringBuilder& sb) { - MOZ_ASSERT(!num->isNegative()); +bool js::temporal::BalanceDateDurationRelative( + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, + TemporalUnit smallestUnit, + Handle> plainRelativeTo, + Handle calendar, DateDuration* result) { + MOZ_ASSERT(plainRelativeTo); + MOZ_ASSERT(calendar.receiver()); - JSLinearString* str = BigInt::toString(cx, num, 10); - if (!str) { - return false; - } - return sb.append(str); + return ::BalanceDateDurationRelative(cx, duration, largestUnit, smallestUnit, + plainRelativeTo, calendar, result); } -static bool NumberToStringBuilder(JSContext* cx, double num, - JSStringBuilder& sb) { - MOZ_ASSERT(IsInteger(num)); - MOZ_ASSERT(num >= 0); - - if (num < DOUBLE_INTEGRAL_PRECISION_LIMIT) { - ToCStringBuf cbuf; - size_t length; - const char* numStr = NumberToCString(&cbuf, num, &length); +/** + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + */ +static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, + Duration* result) { + MOZ_ASSERT(IsValidDuration(one)); + MOZ_ASSERT(IsValidDuration(two)); - return sb.append(numStr, length); - } + // Steps 1-2. (Not applicable) - Rooted bi(cx, BigInt::createFromDouble(cx, num)); - if (!bi) { - return false; - } - return BigIntToStringBuilder(cx, bi, sb); -} + // Step 3. + auto largestUnit1 = DefaultTemporalLargestUnit(one); -static Duration AbsoluteDuration(const Duration& duration) { - return { - std::abs(duration.years), std::abs(duration.months), - std::abs(duration.weeks), std::abs(duration.days), - std::abs(duration.hours), std::abs(duration.minutes), - std::abs(duration.seconds), std::abs(duration.milliseconds), - std::abs(duration.microseconds), std::abs(duration.nanoseconds), - }; -} + // Step 4. + auto largestUnit2 = DefaultTemporalLargestUnit(two); -/** - * FormatFractionalSeconds ( subSecondNanoseconds, precision ) - */ -[[nodiscard]] static bool FormatFractionalSeconds(JSStringBuilder& result, - int32_t subSecondNanoseconds, - Precision precision) { - MOZ_ASSERT(0 <= subSecondNanoseconds && subSecondNanoseconds < 1'000'000'000); - MOZ_ASSERT(precision != Precision::Minute()); + // Step 5. + auto largestUnit = std::min(largestUnit1, largestUnit2); - // Steps 1-2. - if (precision == Precision::Auto()) { - // Step 1.a. - if (subSecondNanoseconds == 0) { - return true; - } + // Step 6. + auto normalized1 = NormalizeTimeDuration(one); - // Step 3. (Reordered) - if (!result.append('.')) { - return false; - } + // Step 7. + auto normalized2 = NormalizeTimeDuration(two); - // Steps 1.b-c. - uint32_t k = 100'000'000; - do { - if (!result.append(char('0' + (subSecondNanoseconds / k)))) { - return false; - } - subSecondNanoseconds %= k; - k /= 10; - } while (subSecondNanoseconds); - } else { - // Step 2.a. - uint8_t p = precision.value(); - if (p == 0) { - return true; - } + // Step 8.a. + if (largestUnit <= TemporalUnit::Week) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, + "relativeTo"); + return false; + } - // Step 3. (Reordered) - if (!result.append('.')) { - return false; - } + // Step 8.b. + NormalizedTimeDuration normalized; + if (!AddNormalizedTimeDuration(cx, normalized1, normalized2, &normalized)) { + return false; + } - // Steps 2.b-c. - uint32_t k = 100'000'000; - for (uint8_t i = 0; i < precision.value(); i++) { - if (!result.append(char('0' + (subSecondNanoseconds / k)))) { - return false; - } - subSecondNanoseconds %= k; - k /= 10; - } + // Step 8.c. + int64_t days1 = mozilla::AssertedCast(one.days); + int64_t days2 = mozilla::AssertedCast(two.days); + auto totalDays = mozilla::CheckedInt64(days1) + days2; + MOZ_ASSERT(totalDays.isValid(), "adding two duration days can't overflow"); + + if (!Add24HourDaysToNormalizedTimeDuration(cx, normalized, totalDays.value(), + &normalized)) { + return false; + } + + // Step 8.d. + TimeDuration balanced; + if (!temporal::BalanceTimeDuration(cx, normalized, largestUnit, &balanced)) { + return false; } + // Steps 8.e. + *result = balanced.toDuration(); return true; } /** - * TemporalDurationToString ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, precision ) + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) */ -static JSString* TemporalDurationToString(JSContext* cx, - const Duration& duration, - Precision precision) { - MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(precision != Precision::Minute()); +static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, + Handle> plainRelativeTo, + Handle calendar, Duration* result) { + MOZ_ASSERT(IsValidDuration(one)); + MOZ_ASSERT(IsValidDuration(two)); - // Convert to absolute values up front. This is okay to do, because when the - // duration is valid, all components have the same sign. - const auto& [years, months, weeks, days, hours, minutes, seconds, - milliseconds, microseconds, nanoseconds] = - AbsoluteDuration(duration); + // Steps 1-2. (Not applicable) - // Fast path for zero durations. - if (years == 0 && months == 0 && weeks == 0 && days == 0 && hours == 0 && - minutes == 0 && seconds == 0 && milliseconds == 0 && microseconds == 0 && - nanoseconds == 0 && - (precision == Precision::Auto() || precision.value() == 0)) { - return NewStringCopyZ(cx, "PT0S"); - } + // Step 3. + auto largestUnit1 = DefaultTemporalLargestUnit(one); - Rooted totalSecondsBigInt(cx); - double totalSeconds = seconds; - int32_t fraction = 0; - if (milliseconds != 0 || microseconds != 0 || nanoseconds != 0) { - bool imprecise = false; - do { - int64_t sec; - int64_t milli; - int64_t micro; - int64_t nano; - if (!mozilla::NumberEqualsInt64(seconds, &sec) || - !mozilla::NumberEqualsInt64(milliseconds, &milli) || - !mozilla::NumberEqualsInt64(microseconds, µ) || - !mozilla::NumberEqualsInt64(nanoseconds, &nano)) { - imprecise = true; - break; - } + // Step 4. + auto largestUnit2 = DefaultTemporalLargestUnit(two); - mozilla::CheckedInt64 intermediate; + // Step 5. + auto largestUnit = std::min(largestUnit1, largestUnit2); - // Step 2. - intermediate = micro; - intermediate += (nano / 1000); - if (!intermediate.isValid()) { - imprecise = true; - break; - } - micro = intermediate.value(); + // Step 6. + auto normalized1 = NormalizeTimeDuration(one); - // Step 3. - nano %= 1000; + // Step 7. + auto normalized2 = NormalizeTimeDuration(two); - // Step 4. - intermediate = milli; - intermediate += (micro / 1000); - if (!intermediate.isValid()) { - imprecise = true; - break; - } - milli = intermediate.value(); + // Step 8. (Not applicable) - // Step 5. - micro %= 1000; + // Step 9.a. (Not applicable in our implementation.) - // Step 6. - intermediate = sec; - intermediate += (milli / 1000); - if (!intermediate.isValid()) { - imprecise = true; - break; - } - sec = intermediate.value(); + // Step 9.b. + auto dateDuration1 = one.toDateDuration(); - // Step 7. - milli %= 1000; + // Step 9.c. + auto dateDuration2 = two.toDateDuration(); - if (sec < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { - totalSeconds = double(sec); - } else { - totalSecondsBigInt = BigInt::createFromInt64(cx, sec); - if (!totalSecondsBigInt) { - return nullptr; - } - } + // Step 9.d. + Rooted> intermediate( + cx, AddDate(cx, calendar, plainRelativeTo, dateDuration1)); + if (!intermediate) { + return false; + } - // These are now all in the range [0, 999]. - MOZ_ASSERT(0 <= milli && milli <= 999); - MOZ_ASSERT(0 <= micro && micro <= 999); - MOZ_ASSERT(0 <= nano && nano <= 999); + // Step 9.e. + Rooted> end( + cx, AddDate(cx, calendar, intermediate, dateDuration2)); + if (!end) { + return false; + } - // Step 20.b. (Reordered) - fraction = milli * 1'000'000 + micro * 1'000 + nano; - MOZ_ASSERT(0 <= fraction && fraction < 1'000'000'000); - } while (false); + // Step 9.f. + auto dateLargestUnit = std::min(TemporalUnit::Day, largestUnit); - // If a result was imprecise, recompute with BigInt to get full precision. - if (imprecise) { - Rooted secs(cx, BigInt::createFromDouble(cx, seconds)); - if (!secs) { - return nullptr; - } + // Steps 9.g-i. + DateDuration dateDifference; + if (!DifferenceDate(cx, calendar, plainRelativeTo, end, dateLargestUnit, + &dateDifference)) { + return false; + } - Rooted millis(cx, BigInt::createFromDouble(cx, milliseconds)); - if (!millis) { - return nullptr; - } + // Step 9.j. + NormalizedTimeDuration normalized1WithDays; + if (!Add24HourDaysToNormalizedTimeDuration( + cx, normalized1, dateDifference.days, &normalized1WithDays)) { + return false; + } - Rooted micros(cx, BigInt::createFromDouble(cx, microseconds)); - if (!micros) { - return nullptr; - } + // Step 9.k. + NormalizedTimeDuration normalized; + if (!AddNormalizedTimeDuration(cx, normalized1WithDays, normalized2, + &normalized)) { + return false; + } - Rooted nanos(cx, BigInt::createFromDouble(cx, nanoseconds)); - if (!nanos) { - return nullptr; - } + // Step 9.l. + TimeDuration balanced; + if (!temporal::BalanceTimeDuration(cx, normalized, largestUnit, &balanced)) { + return false; + } - Rooted thousand(cx, BigInt::createFromInt64(cx, 1000)); - if (!thousand) { - return nullptr; - } + // Steps 9.m. + *result = { + double(dateDifference.years), double(dateDifference.months), + double(dateDifference.weeks), double(balanced.days), + double(balanced.hours), double(balanced.minutes), + double(balanced.seconds), double(balanced.milliseconds), + balanced.microseconds, balanced.nanoseconds, + }; + MOZ_ASSERT(IsValidDuration(*result)); + return true; +} - // Steps 2-3. - Rooted quotient(cx); - if (!BigInt::divmod(cx, nanos, thousand, "ient, &nanos)) { - return nullptr; - } +/** + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + */ +static bool AddDuration( + JSContext* cx, const Duration& one, const Duration& two, + Handle zonedRelativeTo, Handle calendar, + Handle timeZone, + mozilla::Maybe precalculatedPlainDateTime, + Duration* result) { + // Steps 1-2. (Not applicable) - micros = BigInt::add(cx, micros, quotient); - if (!micros) { - return nullptr; - } + // Step 3. + auto largestUnit1 = DefaultTemporalLargestUnit(one); - // Steps 4-5. - if (!BigInt::divmod(cx, micros, thousand, "ient, µs)) { - return nullptr; - } + // Step 4. + auto largestUnit2 = DefaultTemporalLargestUnit(two); - millis = BigInt::add(cx, millis, quotient); - if (!millis) { - return nullptr; - } + // Step 5. + auto largestUnit = std::min(largestUnit1, largestUnit2); - // Steps 6-7. - if (!BigInt::divmod(cx, millis, thousand, "ient, &millis)) { - return nullptr; - } + // Step 6. + auto normalized1 = NormalizeTimeDuration(one); - totalSecondsBigInt = BigInt::add(cx, secs, quotient); - if (!totalSecondsBigInt) { - return nullptr; - } + // Step 7. + auto normalized2 = NormalizeTimeDuration(two); - // These are now all in the range [0, 999]. - int64_t milli = BigInt::toInt64(millis); - int64_t micro = BigInt::toInt64(micros); - int64_t nano = BigInt::toInt64(nanos); + // Steps 8-9. (Not applicable) - MOZ_ASSERT(0 <= milli && milli <= 999); - MOZ_ASSERT(0 <= micro && micro <= 999); - MOZ_ASSERT(0 <= nano && nano <= 999); + // Steps 10-11. (Not applicable in our implementation.) - // Step 20.b. (Reordered) - fraction = milli * 1'000'000 + micro * 1'000 + nano; - MOZ_ASSERT(0 <= fraction && fraction < 1'000'000'000); - } - } + // Step 12. + bool startDateTimeNeeded = largestUnit <= TemporalUnit::Day; - // Steps 8 and 13. - JSStringBuilder result(cx); + // Steps 13-17. + if (!startDateTimeNeeded) { + // Steps 13-14. (Not applicable) - // Step 1. (Reordered) - int32_t sign = DurationSign(duration); + // Step 15. (Inlined AddZonedDateTime, step 6.) + Instant intermediateNs; + if (!AddInstant(cx, zonedRelativeTo.instant(), normalized1, + &intermediateNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); - // Step 21. (Reordered) - if (sign < 0) { - if (!result.append('-')) { - return nullptr; + // Step 16. (Inlined AddZonedDateTime, step 6.) + Instant endNs; + if (!AddInstant(cx, intermediateNs, normalized2, &endNs)) { + return false; } - } + MOZ_ASSERT(IsValidEpochInstant(endNs)); - // Step 22. (Reordered) - if (!result.append('P')) { - return nullptr; - } + // Step 17.a. + auto normalized = NormalizedTimeDurationFromEpochNanosecondsDifference( + endNs, zonedRelativeTo.instant()); - // Step 9. - if (years != 0) { - if (!NumberToStringBuilder(cx, years, result)) { - return nullptr; - } - if (!result.append('Y')) { - return nullptr; + // Step 17.b. + TimeDuration balanced; + if (!BalanceTimeDuration(cx, normalized, largestUnit, &balanced)) { + return false; } - } - // Step 10. - if (months != 0) { - if (!NumberToStringBuilder(cx, months, result)) { - return nullptr; - } - if (!result.append('M')) { - return nullptr; - } + // Step 17.c. + *result = balanced.toDuration(); + return true; } - // Step 11. - if (weeks != 0) { - if (!NumberToStringBuilder(cx, weeks, result)) { - return nullptr; - } - if (!result.append('W')) { - return nullptr; + // Steps 13-14. + PlainDateTime startDateTime; + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, zonedRelativeTo.instant(), + &startDateTime)) { + return false; } + } else { + startDateTime = *precalculatedPlainDateTime; } - // Step 12. - if (days != 0) { - if (!NumberToStringBuilder(cx, days, result)) { - return nullptr; - } - if (!result.append('D')) { - return nullptr; - } + // Step 15. + auto norm1 = + CreateNormalizedDurationRecord(one.toDateDuration(), normalized1); + Instant intermediateNs; + if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, + norm1, startDateTime, &intermediateNs)) { + return false; } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); - // Steps 16-17. - bool nonzeroSecondsAndLower = seconds != 0 || milliseconds != 0 || - microseconds != 0 || nanoseconds != 0; - MOZ_ASSERT(nonzeroSecondsAndLower == - (totalSeconds != 0 || - (totalSecondsBigInt && !totalSecondsBigInt->isZero()) || - fraction != 0)); - - // Steps 18-19. - bool zeroMinutesAndHigher = years == 0 && months == 0 && weeks == 0 && - days == 0 && hours == 0 && minutes == 0; - - // Step 20. (if-condition) - bool hasSecondsPart = nonzeroSecondsAndLower || zeroMinutesAndHigher || - precision != Precision::Auto(); - - if (hours != 0 || minutes != 0 || hasSecondsPart) { - // Step 23. (Reordered) - if (!result.append('T')) { - return nullptr; - } - - // Step 14. - if (hours != 0) { - if (!NumberToStringBuilder(cx, hours, result)) { - return nullptr; - } - if (!result.append('H')) { - return nullptr; - } - } - - // Step 15. - if (minutes != 0) { - if (!NumberToStringBuilder(cx, minutes, result)) { - return nullptr; - } - if (!result.append('M')) { - return nullptr; - } - } - - // Step 20. - if (hasSecondsPart) { - // Step 20.a. - if (totalSecondsBigInt) { - if (!BigIntToStringBuilder(cx, totalSecondsBigInt, result)) { - return nullptr; - } - } else { - if (!NumberToStringBuilder(cx, totalSeconds, result)) { - return nullptr; - } - } - - // Step 20.b. (Moved above) + // Step 16. + auto norm2 = + CreateNormalizedDurationRecord(two.toDateDuration(), normalized2); + Instant endNs; + if (!AddZonedDateTime(cx, intermediateNs, timeZone, calendar, norm2, + &endNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(endNs)); - // Step 20.c. - if (!FormatFractionalSeconds(result, fraction, precision)) { - return nullptr; - } + // Step 17. (Not applicable) - // Step 20.d. - if (!result.append('S')) { - return nullptr; - } - } + // Step 18. + NormalizedDuration difference; + if (!DifferenceZonedDateTime(cx, zonedRelativeTo.instant(), endNs, timeZone, + calendar, largestUnit, startDateTime, + &difference)) { + return false; } - // Step 24. - return result.finishString(); + // Step 19. + auto balanced = BalanceTimeDuration(difference.time, TemporalUnit::Hour); + + // Step 20. + *result = { + double(difference.date.years), double(difference.date.months), + double(difference.date.weeks), double(difference.date.days), + double(balanced.hours), double(balanced.minutes), + double(balanced.seconds), double(balanced.milliseconds), + balanced.microseconds, balanced.nanoseconds, + }; + MOZ_ASSERT(IsValidDuration(*result)); + return true; } /** - * ToRelativeTemporalObject ( options ) + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) */ -static bool ToRelativeTemporalObject( - JSContext* cx, Handle options, - MutableHandle> plainRelativeTo, - MutableHandle zonedRelativeTo, - MutableHandle timeZoneRecord) { - // Step 1. - Rooted value(cx); - if (!GetProperty(cx, options, options, cx->names().relativeTo, &value)) { - return false; - } +static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, + Handle zonedRelativeTo, + Handle calendar, + Handle timeZone, Duration* result) { + return AddDuration(cx, one, two, zonedRelativeTo, calendar, timeZone, + mozilla::Nothing(), result); +} - // Step 2. - if (value.isUndefined()) { - // FIXME: spec issue - switch return record fields for consistency. - // FIXME: spec bug - [[TimeZoneRec]] field not created +/** + * AdjustRoundedDurationDays ( years, months, weeks, days, norm, increment, + * unit, roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * precalculatedPlainDateTime ) + */ +static bool AdjustRoundedDurationDays( + JSContext* cx, const NormalizedDuration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + Handle zonedRelativeTo, Handle calendar, + Handle timeZone, + mozilla::Maybe precalculatedPlainDateTime, + NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); - plainRelativeTo.set(nullptr); - zonedRelativeTo.set(ZonedDateTime{}); - timeZoneRecord.set(TimeZoneRecord{}); + // Step 1. + if ((TemporalUnit::Year <= unit && unit <= TemporalUnit::Day) || + (unit == TemporalUnit::Nanosecond && increment == Increment{1})) { + *result = duration; return true; } - // Step 3. - auto offsetBehaviour = OffsetBehaviour::Option; + // The increment is limited for all smaller temporal units. + MOZ_ASSERT(increment < MaximumTemporalDurationRoundingIncrement(unit)); - // Step 4. - auto matchBehaviour = MatchBehaviour::MatchExactly; + // Step 2. + MOZ_ASSERT(precalculatedPlainDateTime); - // Steps 5-6. - PlainDateTime dateTime; - Rooted calendar(cx); - Rooted timeZone(cx); - int64_t offsetNs; - if (value.isObject()) { - Rooted obj(cx, &value.toObject()); + // Step 3. + int32_t direction = NormalizedTimeDurationSign(duration.time); - // Step 5.a. - if (auto* zonedDateTime = obj->maybeUnwrapIf()) { - auto instant = ToInstant(zonedDateTime); - Rooted timeZone(cx, zonedDateTime->timeZone()); - Rooted calendar(cx, zonedDateTime->calendar()); + // Steps 4-5. + Instant dayStart; + if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, + duration.date, *precalculatedPlainDateTime, + &dayStart)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(dayStart)); - if (!timeZone.wrap(cx)) { - return false; - } - if (!calendar.wrap(cx)) { - return false; - } + // Step 6. + PlainDateTime dayStartDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, dayStart, &dayStartDateTime)) { + return false; + } - // Step 5.a.i. - Rooted timeZoneRec(cx); - if (!CreateTimeZoneMethodsRecord( - cx, timeZone, - { - TimeZoneMethod::GetOffsetNanosecondsFor, - TimeZoneMethod::GetPossibleInstantsFor, - }, - &timeZoneRec)) { - return false; - } + // Step 7. + Instant dayEnd; + if (!AddDaysToZonedDateTime(cx, dayStart, dayStartDateTime, timeZone, + zonedRelativeTo.calendar(), direction, &dayEnd)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(dayEnd)); - // Step 5.a.ii. - plainRelativeTo.set(nullptr); - zonedRelativeTo.set(ZonedDateTime{instant, timeZone, calendar}); - timeZoneRecord.set(timeZoneRec); - return true; - } + // Step 8. + auto dayLengthNs = + NormalizedTimeDurationFromEpochNanosecondsDifference(dayEnd, dayStart); + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs.to())); - // Step 5.b. - if (obj->canUnwrapAs()) { - plainRelativeTo.set(obj); - zonedRelativeTo.set(ZonedDateTime{}); - timeZoneRecord.set(TimeZoneRecord{}); - return true; - } + // Step 9. + NormalizedTimeDuration oneDayLess; + if (!SubtractNormalizedTimeDuration(cx, duration.time, dayLengthNs, + &oneDayLess)) { + return false; + } - // Step 5.c. - if (auto* dateTime = obj->maybeUnwrapIf()) { - auto plainDateTime = ToPlainDate(dateTime); + // Step 10. + int32_t oneDayLessSign = NormalizedTimeDurationSign(oneDayLess); + if ((direction > 0 && oneDayLessSign < 0) || + (direction < 0 && oneDayLessSign > 0)) { + *result = duration; + return true; + } - Rooted calendar(cx, dateTime->calendar()); - if (!calendar.wrap(cx)) { - return false; - } + // Step 11. + Duration adjustedDateDuration; + if (!AddDuration(cx, duration.date.toDuration(), {0, 0, 0, double(direction)}, + zonedRelativeTo, calendar, timeZone, + precalculatedPlainDateTime, &adjustedDateDuration)) { + return false; + } - // Step 5.c.i. - auto* plainDate = CreateTemporalDate(cx, plainDateTime, calendar); - if (!plainDate) { - return false; - } + // Step 12. + NormalizedTimeDuration roundedTime; + if (!RoundDuration(cx, oneDayLess, increment, unit, roundingMode, + &roundedTime)) { + return false; + } - // Step 5.c.ii. - plainRelativeTo.set(plainDate); - zonedRelativeTo.set(ZonedDateTime{}); - timeZoneRecord.set(TimeZoneRecord{}); - return true; - } + // Step 13. + return CombineDateAndNormalizedTimeDuration( + cx, adjustedDateDuration.toDateDuration(), roundedTime, result); +} - // Step 5.d. - if (!GetTemporalCalendarWithISODefault(cx, obj, &calendar)) { - return false; - } +/** + * AdjustRoundedDurationDays ( years, months, weeks, days, norm, increment, + * unit, roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * precalculatedPlainDateTime ) + */ +bool js::temporal::AdjustRoundedDurationDays( + JSContext* cx, const NormalizedDuration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + Handle zonedRelativeTo, Handle calendar, + Handle timeZone, + const PlainDateTime& precalculatedPlainDateTime, + NormalizedDuration* result) { + return ::AdjustRoundedDurationDays( + cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, + timeZone, mozilla::SomeRef(precalculatedPlainDateTime), result); +} - // Step 5.e. - Rooted calendarRec(cx); - if (!CreateCalendarMethodsRecord(cx, calendar, - { - CalendarMethod::DateFromFields, - CalendarMethod::Fields, - }, - &calendarRec)) { - return false; - } +static bool NumberToStringBuilder(JSContext* cx, double num, + JSStringBuilder& sb) { + MOZ_ASSERT(IsInteger(num)); + MOZ_ASSERT(num >= 0); + MOZ_ASSERT(num < DOUBLE_INTEGRAL_PRECISION_LIMIT); - // Step 5.f. - JS::RootedVector fieldNames(cx); - if (!CalendarFields(cx, calendarRec, - {CalendarField::Day, CalendarField::Month, - CalendarField::MonthCode, CalendarField::Year}, - &fieldNames)) { - return false; - } + ToCStringBuf cbuf; + size_t length; + const char* numStr = NumberToCString(&cbuf, num, &length); - // Step 5.g. - if (!AppendSorted(cx, fieldNames.get(), - { - TemporalField::Hour, - TemporalField::Microsecond, - TemporalField::Millisecond, - TemporalField::Minute, - TemporalField::Nanosecond, - TemporalField::Offset, - TemporalField::Second, - TemporalField::TimeZone, - })) { - return false; - } + return sb.append(numStr, length); +} - // Step 5.h. - Rooted fields(cx, PrepareTemporalFields(cx, obj, fieldNames)); - if (!fields) { - return false; - } +static Duration AbsoluteDuration(const Duration& duration) { + return { + std::abs(duration.years), std::abs(duration.months), + std::abs(duration.weeks), std::abs(duration.days), + std::abs(duration.hours), std::abs(duration.minutes), + std::abs(duration.seconds), std::abs(duration.milliseconds), + std::abs(duration.microseconds), std::abs(duration.nanoseconds), + }; +} - // Step 5.i. - Rooted dateOptions(cx, NewPlainObjectWithProto(cx, nullptr)); - if (!dateOptions) { - return false; - } +/** + * FormatFractionalSeconds ( subSecondNanoseconds, precision ) + */ +[[nodiscard]] static bool FormatFractionalSeconds(JSStringBuilder& result, + int32_t subSecondNanoseconds, + Precision precision) { + MOZ_ASSERT(0 <= subSecondNanoseconds && subSecondNanoseconds < 1'000'000'000); + MOZ_ASSERT(precision != Precision::Minute()); - // Step 5.j. - Rooted overflow(cx, StringValue(cx->names().constrain)); - if (!DefineDataProperty(cx, dateOptions, cx->names().overflow, overflow)) { - return false; + // Steps 1-2. + if (precision == Precision::Auto()) { + // Step 1.a. + if (subSecondNanoseconds == 0) { + return true; } - // Step 5.k. - if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, dateOptions, - &dateTime)) { + // Step 3. (Reordered) + if (!result.append('.')) { return false; } - // Step 5.l. - Rooted offset(cx); - if (!GetProperty(cx, fields, fields, cx->names().offset, &offset)) { - return false; + // Steps 1.b-c. + int32_t k = 100'000'000; + do { + if (!result.append(char('0' + (subSecondNanoseconds / k)))) { + return false; + } + subSecondNanoseconds %= k; + k /= 10; + } while (subSecondNanoseconds); + } else { + // Step 2.a. + uint8_t p = precision.value(); + if (p == 0) { + return true; } - // Step 5.m. - Rooted timeZoneValue(cx); - if (!GetProperty(cx, fields, fields, cx->names().timeZone, - &timeZoneValue)) { + // Step 3. (Reordered) + if (!result.append('.')) { return false; } - // Step 5.n. - if (!timeZoneValue.isUndefined()) { - if (!ToTemporalTimeZone(cx, timeZoneValue, &timeZone)) { + // Steps 2.b-c. + int32_t k = 100'000'000; + for (uint8_t i = 0; i < precision.value(); i++) { + if (!result.append(char('0' + (subSecondNanoseconds / k)))) { return false; } + subSecondNanoseconds %= k; + k /= 10; } + } - // Step 5.o. - if (offset.isUndefined()) { - offsetBehaviour = OffsetBehaviour::Wall; - } - - // Steps 8-9. - if (timeZone) { - if (offsetBehaviour == OffsetBehaviour::Option) { - MOZ_ASSERT(!offset.isUndefined()); - MOZ_ASSERT(offset.isString()); + return true; +} - // Step 8.a. - Rooted offsetString(cx, offset.toString()); - if (!offsetString) { - return false; - } +/** + * TemporalDurationToString ( years, months, weeks, days, hours, minutes, + * normSeconds, precision ) + */ +static JSString* TemporalDurationToString(JSContext* cx, + const Duration& duration, + Precision precision) { + MOZ_ASSERT(IsValidDuration(duration)); + MOZ_ASSERT(precision != Precision::Minute()); - // Step 8.b. - if (!ParseDateTimeUTCOffset(cx, offsetString, &offsetNs)) { - return false; - } - } else { - // Step 9. - offsetNs = 0; - } - } - } else { - // Step 6.a. - if (!value.isString()) { - ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, value, - nullptr, "not a string"); - return false; - } - Rooted string(cx, value.toString()); + // Fast path for zero durations. + if (duration == Duration{} && + (precision == Precision::Auto() || precision.value() == 0)) { + return NewStringCopyZ(cx, "PT0S"); + } - // Step 6.b. - bool isUTC; - bool hasOffset; - int64_t timeZoneOffset; - Rooted timeZoneName(cx); - Rooted calendarString(cx); - if (!ParseTemporalRelativeToString(cx, string, &dateTime, &isUTC, - &hasOffset, &timeZoneOffset, - &timeZoneName, &calendarString)) { - return false; - } + // Convert to absolute values up front. This is okay to do, because when the + // duration is valid, all components have the same sign. + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = + AbsoluteDuration(duration); - // Step 6.c. (Not applicable in our implementation.) + // Years to seconds parts are all safe integers for valid durations. + MOZ_ASSERT(years < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(months < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(weeks < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(days < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(hours < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(minutes < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(seconds < DOUBLE_INTEGRAL_PRECISION_LIMIT); - // Steps 6.e-f. - if (timeZoneName) { - // Step 6.f.i. - if (!ToTemporalTimeZone(cx, timeZoneName, &timeZone)) { - return false; - } + auto secondsDuration = NormalizeTimeDuration(0.0, 0.0, seconds, milliseconds, + microseconds, nanoseconds); - // Steps 6.f.ii-iii. - if (isUTC) { - offsetBehaviour = OffsetBehaviour::Exact; - } else if (!hasOffset) { - offsetBehaviour = OffsetBehaviour::Wall; - } + // Step 1. + int32_t sign = DurationSign(duration); - // Step 6.f.iv. - matchBehaviour = MatchBehaviour::MatchMinutes; - } else { - MOZ_ASSERT(!timeZone); - } + // Steps 2 and 7. + JSStringBuilder result(cx); - // Steps 6.g-j. - if (calendarString) { - if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { - return false; - } - } else { - calendar.set(CalendarValue(cx->names().iso8601)); + // Step 13. (Reordered) + if (sign < 0) { + if (!result.append('-')) { + return nullptr; } + } - // Steps 8-9. - if (timeZone) { - if (offsetBehaviour == OffsetBehaviour::Option) { - MOZ_ASSERT(hasOffset); + // Step 14. (Reordered) + if (!result.append('P')) { + return nullptr; + } - // Step 8.a. - offsetNs = timeZoneOffset; - } else { - // Step 9. - offsetNs = 0; - } + // Step 3. + if (years != 0) { + if (!NumberToStringBuilder(cx, years, result)) { + return nullptr; + } + if (!result.append('Y')) { + return nullptr; } } - // Step 7. - if (!timeZone) { - // Step 7.a. - auto* plainDate = CreateTemporalDate(cx, dateTime.date, calendar); - if (!plainDate) { - return false; + // Step 4. + if (months != 0) { + if (!NumberToStringBuilder(cx, months, result)) { + return nullptr; + } + if (!result.append('M')) { + return nullptr; } - - plainRelativeTo.set(plainDate); - zonedRelativeTo.set(ZonedDateTime{}); - timeZoneRecord.set(TimeZoneRecord{}); - return true; } - // Steps 8-9. (Moved above) - - // Step 10. - Rooted timeZoneRec(cx); - if (!CreateTimeZoneMethodsRecord(cx, timeZone, - { - TimeZoneMethod::GetOffsetNanosecondsFor, - TimeZoneMethod::GetPossibleInstantsFor, - }, - &timeZoneRec)) { - return false; + // Step 5. + if (weeks != 0) { + if (!NumberToStringBuilder(cx, weeks, result)) { + return nullptr; + } + if (!result.append('W')) { + return nullptr; + } } - // Step 11. - Instant epochNanoseconds; - if (!InterpretISODateTimeOffset( - cx, dateTime, offsetBehaviour, offsetNs, timeZoneRec, - TemporalDisambiguation::Compatible, TemporalOffset::Reject, - matchBehaviour, &epochNanoseconds)) { - return false; + // Step 6. + if (days != 0) { + if (!NumberToStringBuilder(cx, days, result)) { + return nullptr; + } + if (!result.append('D')) { + return nullptr; + } } - MOZ_ASSERT(IsValidEpochInstant(epochNanoseconds)); - // Step 12. - plainRelativeTo.set(nullptr); - zonedRelativeTo.set(ZonedDateTime{epochNanoseconds, timeZone, calendar}); - timeZoneRecord.set(timeZoneRec); - return true; -} + // Step 7. (Moved above) -/** - * CreateCalendarMethodsRecordFromRelativeTo ( plainRelativeTo, zonedRelativeTo, - * methods ) - */ -static bool CreateCalendarMethodsRecordFromRelativeTo( - JSContext* cx, Handle> plainRelativeTo, - Handle zonedRelativeTo, - mozilla::EnumSet methods, - MutableHandle result) { - // Step 1. - if (zonedRelativeTo) { - return CreateCalendarMethodsRecord(cx, zonedRelativeTo.calendar(), methods, - result); - } + // Steps 10-11. (Reordered) + bool zeroMinutesAndHigher = years == 0 && months == 0 && weeks == 0 && + days == 0 && hours == 0 && minutes == 0; - // Step 2. - if (plainRelativeTo) { - auto* unwrapped = plainRelativeTo.unwrap(cx); - if (!unwrapped) { - return false; + // Steps 8-9, 12, and 15. + bool hasSecondsPart = (secondsDuration != NormalizedTimeDuration{}) || + zeroMinutesAndHigher || precision != Precision::Auto(); + if (hours != 0 || minutes != 0 || hasSecondsPart) { + // Step 15. (Reordered) + if (!result.append('T')) { + return nullptr; } - Rooted calendar(cx, unwrapped->calendar()); - if (!calendar.wrap(cx)) { - return false; + // Step 8. + if (hours != 0) { + if (!NumberToStringBuilder(cx, hours, result)) { + return nullptr; + } + if (!result.append('H')) { + return nullptr; + } } - return CreateCalendarMethodsRecord(cx, calendar, methods, result); - } + // Step 9. + if (minutes != 0) { + if (!NumberToStringBuilder(cx, minutes, result)) { + return nullptr; + } + if (!result.append('M')) { + return nullptr; + } + } - // Step 3. - return true; -} + // Step 12. + if (hasSecondsPart) { + // Step 12.a. + if (!NumberToStringBuilder(cx, double(secondsDuration.seconds), result)) { + return nullptr; + } -static constexpr bool IsSafeInteger(int64_t x) { - constexpr int64_t MaxSafeInteger = int64_t(1) << 53; - constexpr int64_t MinSafeInteger = -MaxSafeInteger; - return MinSafeInteger < x && x < MaxSafeInteger; -} + // Step 12.b. + if (!FormatFractionalSeconds(result, secondsDuration.nanoseconds, + precision)) { + return nullptr; + } -/** - * RoundNumberToIncrement ( x, increment, roundingMode ) - */ -static void TruncateNumber(int64_t numerator, int64_t denominator, - double* quotient, double* total) { - // Computes the quotient and real number value of the rational number - // |numerator / denominator|. - - // Int64 division truncates. - int64_t q = numerator / denominator; - int64_t r = numerator % denominator; - - // The total value is stored as a mathematical number in the draft proposal, - // so we can't convert it to a double without loss of precision. We use two - // different approaches to compute the total value based on the input range. - // - // For example: - // - // When |numerator = 1000001| and |denominator = 60 * 1000|, the exact result - // is |16.66668333...| and the best possible approximation is - // |16.666683333333335070...𝔽|. We can this approximation when casting both - // numerator and denominator to doubles and then performing a double division. - // - // When |numerator = 14400000000000001| and |denominator = 3600000000000|, we - // can't use double division, because |14400000000000001| can't be represented - // as an exact double value. The exact result is |4000.0000000000002777...|. - // - // The best possible approximation is |4000.0000000000004547...𝔽|, which can - // be computed through |q + r / denominator|. - if (::IsSafeInteger(numerator) && ::IsSafeInteger(denominator)) { - *quotient = double(q); - *total = double(numerator) / double(denominator); - } else { - *quotient = double(q); - *total = double(q) + double(r) / double(denominator); + // Step 12.c. + if (!result.append('S')) { + return nullptr; + } + } } + + // Steps 13-15. (Moved above) + + // Step 16. + return result.finishString(); } /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * ToRelativeTemporalObject ( options ) */ -static bool TruncateNumber(JSContext* cx, Handle numerator, - Handle denominator, double* quotient, - double* total) { - MOZ_ASSERT(!denominator->isNegative()); - MOZ_ASSERT(!denominator->isZero()); - - // Dividing zero is always zero. - if (numerator->isZero()) { - *quotient = 0; - *total = 0; - return true; +static bool ToRelativeTemporalObject( + JSContext* cx, Handle options, + MutableHandle> plainRelativeTo, + MutableHandle zonedRelativeTo, + MutableHandle timeZoneRecord) { + // Step 1. + Rooted value(cx); + if (!GetProperty(cx, options, options, cx->names().relativeTo, &value)) { + return false; } - int64_t num, denom; - if (BigInt::isInt64(numerator, &num) && - BigInt::isInt64(denominator, &denom)) { - TruncateNumber(num, denom, quotient, total); + // Step 2. + if (value.isUndefined()) { + plainRelativeTo.set(nullptr); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); return true; } - // BigInt division truncates. - Rooted quot(cx); - Rooted rem(cx); - if (!BigInt::divmod(cx, numerator, denominator, ", &rem)) { - return false; - } - - double q = BigInt::numberValue(quot); - *quotient = q; - *total = q + BigInt::numberValue(rem) / BigInt::numberValue(denominator); - return true; -} - -/** - * RoundNumberToIncrement ( x, increment, roundingMode ) - */ -static bool TruncateNumber(JSContext* cx, const Duration& toRound, - TemporalUnit unit, double* quotient, double* total) { - MOZ_ASSERT(unit >= TemporalUnit::Day); + // Step 3. + auto offsetBehaviour = OffsetBehaviour::Option; - int64_t denominator = ToNanoseconds(unit); - MOZ_ASSERT(denominator > 0); - MOZ_ASSERT(denominator <= 86'400'000'000'000); + // Step 4. + auto matchBehaviour = MatchBehaviour::MatchExactly; - // Fast-path when we can perform the whole computation with int64 values. - if (auto numerator = TotalDurationNanoseconds(toRound)) { - TruncateNumber(*numerator, denominator, quotient, total); - return true; - } + // Steps 5-6. + PlainDateTime dateTime; + Rooted calendar(cx); + Rooted timeZone(cx); + int64_t offsetNs; + if (value.isObject()) { + Rooted obj(cx, &value.toObject()); - Rooted numerator(cx, TotalDurationNanosecondsSlow(cx, toRound)); - if (!numerator) { - return false; - } + // Step 5.a. + if (auto* zonedDateTime = obj->maybeUnwrapIf()) { + auto instant = ToInstant(zonedDateTime); + Rooted timeZone(cx, zonedDateTime->timeZone()); + Rooted calendar(cx, zonedDateTime->calendar()); - // Division by one has no remainder. - if (denominator == 1) { - double q = BigInt::numberValue(numerator); - *quotient = q; - *total = q; - return true; - } + if (!timeZone.wrap(cx)) { + return false; + } + if (!calendar.wrap(cx)) { + return false; + } - Rooted denom(cx, BigInt::createFromInt64(cx, denominator)); - if (!denom) { - return false; - } + // Step 5.a.i. + Rooted timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord( + cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } - // BigInt division truncates. - Rooted quot(cx); - Rooted rem(cx); - if (!BigInt::divmod(cx, numerator, denom, ", &rem)) { - return false; - } + // Step 5.a.ii. + plainRelativeTo.set(nullptr); + zonedRelativeTo.set(ZonedDateTime{instant, timeZone, calendar}); + timeZoneRecord.set(timeZoneRec); + return true; + } - double q = BigInt::numberValue(quot); - *quotient = q; - *total = q + BigInt::numberValue(rem) / double(denominator); - return true; -} + // Step 5.b. + if (obj->canUnwrapAs()) { + plainRelativeTo.set(obj); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); + return true; + } -/** - * RoundNumberToIncrement ( x, increment, roundingMode ) - */ -static bool RoundNumberToIncrement(JSContext* cx, const Duration& toRound, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, - double* result) { - MOZ_ASSERT(unit >= TemporalUnit::Day); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto total = TotalDurationNanoseconds(toRound)) { - return RoundNumberToIncrement(cx, *total, unit, increment, roundingMode, - result); - } + // Step 5.c. + if (auto* dateTime = obj->maybeUnwrapIf()) { + auto plainDateTime = ToPlainDate(dateTime); - Rooted totalNs(cx, TotalDurationNanosecondsSlow(cx, toRound)); - if (!totalNs) { - return false; - } + Rooted calendar(cx, dateTime->calendar()); + if (!calendar.wrap(cx)) { + return false; + } - return RoundNumberToIncrement(cx, totalNs, unit, increment, roundingMode, - result); -} + // Step 5.c.i. + auto* plainDate = CreateTemporalDate(cx, plainDateTime, calendar); + if (!plainDate) { + return false; + } -struct RoundedDuration final { - Duration duration; - double total = 0; -}; + // Step 5.c.ii. + plainRelativeTo.set(plainDate); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); + return true; + } -enum class ComputeRemainder : bool { No, Yes }; + // Step 5.d. + if (!GetTemporalCalendarWithISODefault(cx, obj, &calendar)) { + return false; + } -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -static bool RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, - RoundedDuration* result) { - // The remainder is only needed when called from |Duration_total|. And `total` - // always passes |increment=1| and |roundingMode=trunc|. - MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, - increment == Increment{1}); - MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, - roundingMode == TemporalRoundingMode::Trunc); + // Step 5.e. + Rooted calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + }, + &calendarRec)) { + return false; + } - auto [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + // Step 5.f. + JS::RootedVector fieldNames(cx); + if (!CalendarFields(cx, calendarRec, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } - // Steps 1-5. (Not applicable.) + // Step 5.g. + if (!AppendSorted(cx, fieldNames.get(), + { + TemporalField::Hour, + TemporalField::Microsecond, + TemporalField::Millisecond, + TemporalField::Minute, + TemporalField::Nanosecond, + TemporalField::Offset, + TemporalField::Second, + TemporalField::TimeZone, + })) { + return false; + } - // Step 6. - if (unit <= TemporalUnit::Week) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, - "relativeTo"); - return false; - } + // Step 5.h. + Rooted fields(cx, PrepareTemporalFields(cx, obj, fieldNames)); + if (!fields) { + return false; + } - // TODO: We could directly return here if unit=nanoseconds and increment=1, - // because in that case this operation is a no-op. This case happens for - // example when calling Temporal.PlainTime.prototype.{since,until} without an - // options object. - // - // But maybe this can be even more efficiently handled in the callers. For - // example when Temporal.PlainTime.prototype.{since,until} is called without - // an options object, we can not only skip the RoundDuration call, but also - // the following BalanceTimeDuration call. + // Step 5.i. + Rooted dateOptions(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!dateOptions) { + return false; + } - // Step 7. (Not applicable.) + // Step 5.j. + Rooted overflow(cx, StringValue(cx->names().constrain)); + if (!DefineDataProperty(cx, dateOptions, cx->names().overflow, overflow)) { + return false; + } - // Step 8. (Moved below.) + // Step 5.k. + if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, dateOptions, + &dateTime)) { + return false; + } - // Step 9. (Not applicable.) + // Step 5.l. + Rooted offset(cx); + if (!GetProperty(cx, fields, fields, cx->names().offset, &offset)) { + return false; + } - // Steps 10-19. - Duration toRound; - double* roundedTime; - switch (unit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Week: - case TemporalUnit::Month: - // Steps 10-12. (Not applicable.) - MOZ_CRASH("Unexpected temporal unit"); + // Step 5.m. + Rooted timeZoneValue(cx); + if (!GetProperty(cx, fields, fields, cx->names().timeZone, + &timeZoneValue)) { + return false; + } - case TemporalUnit::Day: { - // clang-format off - // - // Relevant steps from the spec algorithm: - // - // 6.a Let nanoseconds be ! TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 0). - // 6.d Let result be ? NanosecondsToDays(nanoseconds, intermediate). - // 6.e Set days to days + result.[[Days]] + result.[[Nanoseconds]] / abs(result.[[DayLength]]). - // ... - // 12.a Let fractionalDays be days. - // 12.b Set days to ? RoundNumberToIncrement(days, increment, roundingMode). - // 12.c Set remainder to fractionalDays - days. - // - // Where `result.[[Days]]` is `the integral part of nanoseconds / dayLengthNs` - // and `result.[[Nanoseconds]]` is `nanoseconds modulo dayLengthNs`. - // With `dayLengthNs = 8.64 × 10^13`. - // - // So we have: - // d + r.days + (r.nanoseconds / len) - // = d + [ns / len] + ((ns % len) / len) - // = d + [ns / len] + ((ns - ([ns / len] × len)) / len) - // = d + [ns / len] + (ns / len) - (([ns / len] × len) / len) - // = d + [ns / len] + (ns / len) - [ns / len] - // = d + (ns / len) - // = ((d × len) / len) + (ns / len) - // = ((d × len) + ns) / len - // - // `((d × len) + ns)` is the result of calling TotalDurationNanoseconds(), - // which means we can use the same code for all time computations in this - // function. - // - // clang-format on - - MOZ_ASSERT(increment <= Increment{1'000'000'000}, - "limited by ToTemporalRoundingIncrement"); - - // Steps 7.a, 7.c, and 13.a-b. - toRound = duration; - roundedTime = &days; - - // Step 7.b. (Not applicable) - - // Steps 7.d-e. - hours = 0; - minutes = 0; - seconds = 0; - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; + // Step 5.n. + if (!timeZoneValue.isUndefined()) { + if (!ToTemporalTimeZone(cx, timeZoneValue, &timeZone)) { + return false; + } } - case TemporalUnit::Hour: { - MOZ_ASSERT(increment <= Increment{24}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 8 and 14.a-c. - toRound = { - 0, - 0, - 0, - 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - }; - roundedTime = &hours; - - // Step 14.d. - minutes = 0; - seconds = 0; - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; + // Step 5.o. + if (offset.isUndefined()) { + offsetBehaviour = OffsetBehaviour::Wall; } - case TemporalUnit::Minute: { - MOZ_ASSERT(increment <= Increment{60}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 8 and 15.a-c. - toRound = { - 0, 0, 0, 0, 0, minutes, seconds, milliseconds, microseconds, - nanoseconds, - }; - roundedTime = &minutes; - - // Step 15.d. - seconds = 0; - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; + // Steps 8-9. + if (timeZone) { + if (offsetBehaviour == OffsetBehaviour::Option) { + MOZ_ASSERT(!offset.isUndefined()); + MOZ_ASSERT(offset.isString()); + + // Step 8.a. + Rooted offsetString(cx, offset.toString()); + if (!offsetString) { + return false; + } + + // Step 8.b. + if (!ParseDateTimeUTCOffset(cx, offsetString, &offsetNs)) { + return false; + } + } else { + // Step 9. + offsetNs = 0; + } + } + } else { + // Step 6.a. + if (!value.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, value, + nullptr, "not a string"); + return false; } + Rooted string(cx, value.toString()); - case TemporalUnit::Second: { - MOZ_ASSERT(increment <= Increment{60}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 8 and 16.a-b. - toRound = { - 0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, - }; - roundedTime = &seconds; - - // Step 16.c. - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; + // Step 6.b. + bool isUTC; + bool hasOffset; + int64_t timeZoneOffset; + Rooted timeZoneAnnotation(cx); + Rooted calendarString(cx); + if (!ParseTemporalRelativeToString(cx, string, &dateTime, &isUTC, + &hasOffset, &timeZoneOffset, + &timeZoneAnnotation, &calendarString)) { + return false; } - case TemporalUnit::Millisecond: { - MOZ_ASSERT(increment <= Increment{1000}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 17.a-c. - toRound = {0, 0, 0, 0, 0, 0, 0, milliseconds, microseconds, nanoseconds}; - roundedTime = &milliseconds; - - // Step 17.d. - microseconds = 0; - nanoseconds = 0; - break; - } + // Step 6.c. (Not applicable in our implementation.) - case TemporalUnit::Microsecond: { - MOZ_ASSERT(increment <= Increment{1000}, - "limited by MaximumTemporalDurationRoundingIncrement"); + // Steps 6.e-f. + if (timeZoneAnnotation) { + // Step 6.f.i. + if (!ToTemporalTimeZone(cx, timeZoneAnnotation, &timeZone)) { + return false; + } - // Steps 18.a-c. - toRound = {0, 0, 0, 0, 0, 0, 0, 0, microseconds, nanoseconds}; - roundedTime = µseconds; + // Steps 6.f.ii-iii. + if (isUTC) { + offsetBehaviour = OffsetBehaviour::Exact; + } else if (!hasOffset) { + offsetBehaviour = OffsetBehaviour::Wall; + } - // Step 18.d. - nanoseconds = 0; - break; + // Step 6.f.iv. + matchBehaviour = MatchBehaviour::MatchMinutes; + } else { + MOZ_ASSERT(!timeZone); } - case TemporalUnit::Nanosecond: { - MOZ_ASSERT(increment <= Increment{1000}, - "limited by MaximumTemporalDurationRoundingIncrement"); + // Steps 6.g-j. + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { + return false; + } + } else { + calendar.set(CalendarValue(cx->names().iso8601)); + } - // Step 19.a. (Implicit) + // Steps 8-9. + if (timeZone) { + if (offsetBehaviour == OffsetBehaviour::Option) { + MOZ_ASSERT(hasOffset); - // Steps 19.b-c. - toRound = {0, 0, 0, 0, 0, 0, 0, 0, 0, nanoseconds}; - roundedTime = &nanoseconds; - break; + // Step 8.a. + offsetNs = timeZoneOffset; + } else { + // Step 9. + offsetNs = 0; + } } } - // clang-format off - // - // The specification uses mathematical values in its computations, which - // requires to be able to represent decimals with arbitrary precision. To - // avoid having to struggle with decimals, we can transform the steps to work - // on integer values, which we can conveniently represent with BigInts. - // - // As an example here are the transformation steps for "hours", but all other - // units can be handled similarly. - // - // Relevant spec steps: - // - // 8.a Let fractionalSeconds be nanoseconds × 10^9 + microseconds × 10^6 + milliseconds × 10^3 + seconds. - // ... - // 14.a Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours. - // 14.b Set hours to ? RoundNumberToIncrement(fractionalHours, increment, roundingMode). - // - // And from RoundNumberToIncrement: - // - // 1. Let quotient be x / increment. - // 2-7. Let rounded be op(quotient). - // 8. Return rounded × increment. - // - // With `fractionalHours = (totalNs / nsPerHour)`, the rounding operation - // computes: - // - // op(fractionalHours / increment) × increment - // = op((totalNs / nsPerHour) / increment) × increment - // = op(totalNs / (nsPerHour × increment)) × increment - // - // So when we pass `totalNs` and `nsPerHour` as separate arguments to - // RoundNumberToIncrement, we can avoid any precision losses and instead - // compute with exact values. - // - // clang-format on - - double total = 0; - if (computeRemainder == ComputeRemainder::No) { - if (!RoundNumberToIncrement(cx, toRound, unit, increment, roundingMode, - roundedTime)) { + // Step 7. + if (!timeZone) { + // Step 7.a. + auto* plainDate = CreateTemporalDate(cx, dateTime.date, calendar); + if (!plainDate) { return false; } - } else { - // clang-format off - // - // The remainder is only used for Duration.prototype.total(), which calls - // this operation with increment=1 and roundingMode=trunc. - // - // That means the remainder computation is actually just - // `(totalNs % toNanos) / toNanos`, where `totalNs % toNanos` is already - // computed in RoundNumberToIncrement(): - // - // rounded = trunc(totalNs / toNanos) - // = [totalNs / toNanos] - // - // roundedTime = ℝ(𝔽(rounded)) - // - // remainder = (totalNs - (rounded * toNanos)) / toNanos - // = (totalNs - ([totalNs / toNanos] * toNanos)) / toNanos - // = (totalNs % toNanos) / toNanos - // - // When used in Duration.prototype.total(), the overall computed value is - // `[totalNs / toNanos] + (totalNs % toNanos) / toNanos`. - // - // Applying normal math rules would allow to simplify this to: - // - // [totalNs / toNanos] + (totalNs % toNanos) / toNanos - // = [totalNs / toNanos] + (totalNs - [totalNs / toNanos] * toNanos) / toNanos - // = total / toNanos - // - // We can't apply this simplification because it'd introduce double - // precision issues. Instead of that, we use a specialized version of - // RoundNumberToIncrement which directly returns the remainder. The - // remainder `(totalNs % toNanos) / toNanos` is a value near zero, so this - // approach is as exact as possible. (Double numbers near zero can be - // computed more precisely than large numbers with fractional parts.) - // - // clang-format on - - MOZ_ASSERT(increment == Increment{1}); - MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); - if (!TruncateNumber(cx, toRound, unit, roundedTime, &total)) { - return false; - } + plainRelativeTo.set(plainDate); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); + return true; } - MOZ_ASSERT(years == duration.years); - MOZ_ASSERT(months == duration.months); - MOZ_ASSERT(weeks == duration.weeks); - MOZ_ASSERT(IsIntegerOrInfinity(days)); + // Steps 8-9. (Moved above) - // Step 20. - Duration resultDuration = {years, months, weeks, days, - hours, minutes, seconds, milliseconds, - microseconds, nanoseconds}; - if (!ThrowIfInvalidDuration(cx, resultDuration)) { + // Step 10. + Rooted timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { return false; } - // Step 21. - *result = {resultDuration, total}; - return true; -} - -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -static bool RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, double* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - // Only called from |Duration_total|, which always passes |increment=1| and - // |roundingMode=trunc|. - MOZ_ASSERT(increment == Increment{1}); - MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); - - RoundedDuration rounded; - if (!::RoundDuration(cx, duration, increment, unit, roundingMode, - ComputeRemainder::Yes, &rounded)) { + // Step 11. + Instant epochNanoseconds; + if (!InterpretISODateTimeOffset( + cx, dateTime, offsetBehaviour, offsetNs, timeZoneRec, + TemporalDisambiguation::Compatible, TemporalOffset::Reject, + matchBehaviour, &epochNanoseconds)) { return false; } + MOZ_ASSERT(IsValidEpochInstant(epochNanoseconds)); - *result = rounded.total; + // Step 12. + plainRelativeTo.set(nullptr); + zonedRelativeTo.set(ZonedDateTime{epochNanoseconds, timeZone, calendar}); + timeZoneRecord.set(timeZoneRec); return true; } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * CreateCalendarMethodsRecordFromRelativeTo ( plainRelativeTo, zonedRelativeTo, + * methods ) */ -static bool RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - RoundedDuration rounded; - if (!::RoundDuration(cx, duration, increment, unit, roundingMode, - ComputeRemainder::No, &rounded)) { - return false; +static bool CreateCalendarMethodsRecordFromRelativeTo( + JSContext* cx, Handle> plainRelativeTo, + Handle zonedRelativeTo, + mozilla::EnumSet methods, + MutableHandle result) { + // Step 1. + if (zonedRelativeTo) { + return CreateCalendarMethodsRecord(cx, zonedRelativeTo.calendar(), methods, + result); } - *result = rounded.duration; - return true; -} - -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -bool js::temporal::RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, - Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - return ::RoundDuration(cx, duration, increment, unit, roundingMode, result); -} - -static mozilla::Maybe DaysFrom( - const temporal::NanosecondsAndDays& nanosAndDays) { - if (auto* days = nanosAndDays.days) { - int64_t daysInt; - if (BigInt::isInt64(days, &daysInt)) { - return mozilla::Some(daysInt); + // Step 2. + if (plainRelativeTo) { + auto* unwrapped = plainRelativeTo.unwrap(cx); + if (!unwrapped) { + return false; } - return mozilla::Nothing(); - } - return mozilla::Some(nanosAndDays.daysInt); -} - -static BigInt* DaysFrom(JSContext* cx, - Handle nanosAndDays) { - if (auto days = nanosAndDays.days()) { - return days; - } - return BigInt::createFromInt64(cx, nanosAndDays.daysInt()); -} -static bool TruncateDays(JSContext* cx, - Handle nanosAndDays, - double days, int32_t daysToAdd, double* result) { - do { - int64_t intDays; - if (!mozilla::NumberEqualsInt64(days, &intDays)) { - break; + Rooted calendar(cx, unwrapped->calendar()); + if (!calendar.wrap(cx)) { + return false; } - auto nanoDays = DaysFrom(nanosAndDays); - if (!nanoDays) { - break; - } + return CreateCalendarMethodsRecord(cx, calendar, methods, result); + } - auto totalDays = mozilla::CheckedInt64(intDays); - totalDays += *nanoDays; - totalDays += daysToAdd; - if (!totalDays.isValid()) { - break; - } + // Step 3. + return true; +} - int64_t truncatedDays = totalDays.value(); - if (nanosAndDays.nanoseconds() > InstantSpan{}) { - // Round toward positive infinity when the integer days are negative and - // the fractional part is positive. - if (truncatedDays < 0) { - truncatedDays += 1; - } - } else if (nanosAndDays.nanoseconds() < InstantSpan{}) { - // Round toward negative infinity when the integer days are positive and - // the fractional part is negative. - if (truncatedDays > 0) { - truncatedDays -= 1; - } - } +struct RoundedDuration final { + NormalizedDuration duration; + double total = 0; +}; - *result = double(truncatedDays); - return true; - } while (false); +enum class ComputeRemainder : bool { No, Yes }; - Rooted biDays(cx, BigInt::createFromDouble(cx, days)); - if (!biDays) { - return false; - } +/** + * RoundNormalizedTimeDurationToIncrement ( d, increment, roundingMode ) + */ +static NormalizedTimeDuration RoundNormalizedTimeDurationToIncrement( + const NormalizedTimeDuration& duration, const TemporalUnit unit, + Increment increment, TemporalRoundingMode roundingMode) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); + MOZ_ASSERT(increment <= MaximumTemporalDurationRoundingIncrement(unit)); - Rooted biNanoDays(cx, DaysFrom(cx, nanosAndDays)); - if (!biNanoDays) { - return false; - } + int64_t divisor = ToNanoseconds(unit) * increment.value(); + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(divisor <= ToNanoseconds(TemporalUnit::Day)); - Rooted biDaysToAdd(cx, BigInt::createFromInt64(cx, daysToAdd)); - if (!biDaysToAdd) { - return false; - } + auto totalNanoseconds = duration.toNanoseconds(); + auto rounded = + RoundNumberToIncrement(totalNanoseconds, Int128{divisor}, roundingMode); + return NormalizedTimeDuration::fromNanoseconds(rounded); +} - Rooted truncatedDays(cx, BigInt::add(cx, biDays, biNanoDays)); - if (!truncatedDays) { - return false; - } +/** + * RoundNormalizedTimeDurationToIncrement ( d, increment, roundingMode ) + */ +static bool RoundNormalizedTimeDurationToIncrement( + JSContext* cx, const NormalizedTimeDuration& duration, + const TemporalUnit unit, Increment increment, + TemporalRoundingMode roundingMode, NormalizedTimeDuration* result) { + // Step 1. + auto rounded = RoundNormalizedTimeDurationToIncrement( + duration, unit, increment, roundingMode); - truncatedDays = BigInt::add(cx, truncatedDays, biDaysToAdd); - if (!truncatedDays) { + // Step 2. + if (!IsValidNormalizedTimeDuration(rounded)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - if (nanosAndDays.nanoseconds() > InstantSpan{}) { - // Round toward positive infinity when the integer days are negative and - // the fractional part is positive. - if (truncatedDays->isNegative()) { - truncatedDays = BigInt::inc(cx, truncatedDays); - if (!truncatedDays) { - return false; - } - } - } else if (nanosAndDays.nanoseconds() < InstantSpan{}) { - // Round toward negative infinity when the integer days are positive and - // the fractional part is negative. - if (!truncatedDays->isNegative() && !truncatedDays->isZero()) { - truncatedDays = BigInt::dec(cx, truncatedDays); - if (!truncatedDays) { - return false; - } - } - } - - *result = BigInt::numberValue(truncatedDays); + // Step 3. + *result = rounded; return true; } -static bool DaysIsNegative(double days, - Handle nanosAndDays, - int32_t daysToAdd) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; - - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); +/** + * DivideNormalizedTimeDuration ( d, divisor ) + */ +static double TotalNormalizedTimeDuration( + const NormalizedTimeDuration& duration, const TemporalUnit unit) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); - // We don't need the exact value, so it's safe to convert from BigInt. - double nanoDays = nanosAndDays.daysNumber(); + auto numerator = duration.toNanoseconds(); + auto denominator = Int128{ToNanoseconds(unit)}; + return FractionToDouble(numerator, denominator); +} - // When non-zero |days| and |nanoDays| have oppositive signs, the absolute - // value of |days| is less-or-equal to |epochDays|. That means when adding - // |days + nanoDays| we don't have to worry about a case like: - // - // days = 9007199254740991 and - // nanoDays = 𝔽(-9007199254740993) = -9007199254740992 - // - // ℝ(𝔽(days) + 𝔽(nanoDays)) is -1, whereas the correct result is -2. - MOZ_ASSERT((days <= 0 && nanoDays <= 0) || (days >= 0 && nanoDays >= 0) || - std::abs(days) <= epochDays); +/** + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) + */ +NormalizedTimeDuration js::temporal::RoundDuration( + const NormalizedTimeDuration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); - // This addition can be imprecise, so |daysApproximation| is only an - // approximation of the actual value. - double daysApproximation = days + nanoDays; + // Steps 1-12. (Not applicable) - if (std::abs(daysApproximation) <= epochDays * 2) { - int32_t intDays = int32_t(daysApproximation) + daysToAdd; - return intDays < 0 || - (intDays == 0 && nanosAndDays.nanoseconds() < InstantSpan{}); - } + // Steps 13-18. + auto rounded = RoundNormalizedTimeDurationToIncrement( + duration, unit, increment, roundingMode); + MOZ_ASSERT(IsValidNormalizedTimeDuration(rounded)); - // |daysApproximation| is too large, adding |daysToAdd| and |daysToSubtract| - // doesn't change the sign. - return daysApproximation < 0; + // Step 19. + return rounded; } -struct RoundedNumber { - double rounded; - double total; -}; - -static bool RoundNumberToIncrementSlow( - JSContext* cx, double durationAmount, double amountPassed, - double durationDays, int32_t daysToAdd, - Handle nanosAndDays, int32_t oneUnitDays, - Increment increment, TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, RoundedNumber* result) { - MOZ_ASSERT(nanosAndDays.dayLength() > InstantSpan{}); - MOZ_ASSERT(nanosAndDays.nanoseconds().abs() < nanosAndDays.dayLength().abs()); - MOZ_ASSERT(oneUnitDays != 0); +/** + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) + */ +bool js::temporal::RoundDuration(JSContext* cx, + const NormalizedTimeDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); - Rooted biAmount(cx, BigInt::createFromDouble(cx, durationAmount)); - if (!biAmount) { - return false; - } + // Steps 1-12. (Not applicable) - Rooted biAmountPassed(cx, - BigInt::createFromDouble(cx, amountPassed)); - if (!biAmountPassed) { - return false; - } + // Steps 13-19. + return RoundNormalizedTimeDurationToIncrement(cx, duration, unit, increment, + roundingMode, result); +} - biAmount = BigInt::add(cx, biAmount, biAmountPassed); - if (!biAmount) { - return false; - } +#ifdef DEBUG +// Valid duration days are smaller than ⌈(2**53) / (24 * 60 * 60)⌉. +static constexpr int64_t MaxDurationDays = (int64_t(1) << 53) / (24 * 60 * 60); - Rooted days(cx, BigInt::createFromDouble(cx, durationDays)); - if (!days) { - return false; - } +// Maximum number of days in |FractionalDays|. +static constexpr int64_t MaxFractionalDays = + 2 * MaxDurationDays + 2 * MaxEpochDaysDuration; +#endif - Rooted nanoDays(cx, DaysFrom(cx, nanosAndDays)); - if (!nanoDays) { - return false; - } +struct FractionalDays final { + int64_t days = 0; + int64_t time = 0; + int64_t dayLength = 0; - Rooted biDaysToAdd(cx, BigInt::createFromInt64(cx, daysToAdd)); - if (!biDaysToAdd) { - return false; - } + FractionalDays() = default; - days = BigInt::add(cx, days, nanoDays); - if (!days) { - return false; - } + explicit FractionalDays(int64_t durationDays, + const NormalizedTimeAndDays& timeAndDays) + : days(durationDays + timeAndDays.days), + time(timeAndDays.time), + dayLength(timeAndDays.dayLength) { + MOZ_ASSERT(std::abs(durationDays) <= MaxDurationDays); + MOZ_ASSERT(std::abs(timeAndDays.days) <= MaxDurationDays); + MOZ_ASSERT(std::abs(days) <= MaxFractionalDays); - days = BigInt::add(cx, days, biDaysToAdd); - if (!days) { - return false; - } + // NormalizedTimeDurationToDays guarantees that |dayLength| is strictly + // positive and less than 2**53. + MOZ_ASSERT(dayLength > 0); + MOZ_ASSERT(dayLength < int64_t(1) << 53); - Rooted nanoseconds( - cx, ToEpochNanoseconds(cx, nanosAndDays.nanoseconds())); - if (!nanoseconds) { - return false; + // NormalizedTimeDurationToDays guarantees that |abs(timeAndDays.time)| is + // less than |timeAndDays.dayLength|. + MOZ_ASSERT(std::abs(time) < dayLength); } - Rooted dayLength(cx, - ToEpochNanoseconds(cx, nanosAndDays.dayLength())); - if (!dayLength) { - return false; + FractionalDays operator+=(int32_t epochDays) { + MOZ_ASSERT(std::abs(epochDays) <= MaxEpochDaysDuration); + days += epochDays; + MOZ_ASSERT(std::abs(days) <= MaxFractionalDays); + return *this; } - Rooted denominator( - cx, BigInt::createFromInt64(cx, std::abs(oneUnitDays))); - if (!denominator) { - return false; + FractionalDays operator-=(int32_t epochDays) { + MOZ_ASSERT(std::abs(epochDays) <= MaxEpochDaysDuration); + days -= epochDays; + MOZ_ASSERT(std::abs(days) <= MaxFractionalDays); + return *this; } - denominator = BigInt::mul(cx, denominator, dayLength); - if (!denominator) { - return false; + int64_t truncate() const { + int64_t truncatedDays = days; + if (time > 0) { + // Round toward positive infinity when the integer days are negative and + // the fractional part is positive. + if (truncatedDays < 0) { + truncatedDays += 1; + } + } else if (time < 0) { + // Round toward negative infinity when the integer days are positive and + // the fractional part is negative. + if (truncatedDays > 0) { + truncatedDays -= 1; + } + } + MOZ_ASSERT(std::abs(truncatedDays) <= MaxFractionalDays + 1); + return truncatedDays; } - Rooted totalNanoseconds(cx, BigInt::mul(cx, days, dayLength)); - if (!totalNanoseconds) { - return false; + int32_t sign() const { + if (days != 0) { + return days < 0 ? -1 : 1; + } + return time < 0 ? -1 : time > 0 ? 1 : 0; } +}; - totalNanoseconds = BigInt::add(cx, totalNanoseconds, nanoseconds); - if (!totalNanoseconds) { - return false; - } +struct Fraction final { + int64_t numerator = 0; + int32_t denominator = 0; - Rooted amountNanos(cx, BigInt::mul(cx, biAmount, denominator)); - if (!amountNanos) { - return false; - } + constexpr Fraction() = default; - totalNanoseconds = BigInt::add(cx, totalNanoseconds, amountNanos); - if (!totalNanoseconds) { - return false; + constexpr Fraction(int64_t numerator, int32_t denominator) + : numerator(numerator), denominator(denominator) { + MOZ_ASSERT(denominator > 0); } +}; - double rounded; +struct RoundedNumber final { + Int128 rounded; double total = 0; - if (computeRemainder == ComputeRemainder::No) { - if (!temporal::RoundNumberToIncrement(cx, totalNanoseconds, denominator, - increment, roundingMode, &rounded)) { - return false; - } - } else { - if (!::TruncateNumber(cx, totalNanoseconds, denominator, &rounded, - &total)) { - return false; - } - } - - *result = {rounded, total}; - return true; -} +}; -static bool RoundNumberToIncrement( - JSContext* cx, double durationAmount, double amountPassed, - double durationDays, int32_t daysToAdd, - Handle nanosAndDays, int32_t oneUnitDays, +static RoundedNumber RoundNumberToIncrement( + const Fraction& fraction, const FractionalDays& fractionalDays, Increment increment, TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, RoundedNumber* result) { - MOZ_ASSERT(nanosAndDays.dayLength() > InstantSpan{}); - MOZ_ASSERT(nanosAndDays.nanoseconds().abs() < nanosAndDays.dayLength().abs()); - MOZ_ASSERT(oneUnitDays != 0); - - // TODO(anba): Rename variables. + ComputeRemainder computeRemainder) { + MOZ_ASSERT(std::abs(fraction.numerator) < (int64_t(1) << 32) * 2); + MOZ_ASSERT(fraction.denominator > 0); + MOZ_ASSERT(fraction.denominator <= MaxEpochDaysDuration); + MOZ_ASSERT(std::abs(fractionalDays.days) <= MaxFractionalDays); + MOZ_ASSERT(fractionalDays.dayLength > 0); + MOZ_ASSERT(fractionalDays.dayLength < (int64_t(1) << 53)); + MOZ_ASSERT(std::abs(fractionalDays.time) < fractionalDays.dayLength); + MOZ_ASSERT(increment <= Increment::max()); // clang-format off // @@ -4373,7 +3339,7 @@ static bool RoundNumberToIncrement( // // where days' = days + nanoseconds / dayLength. // - // The fractional part |nanoseconds / dayLength| is from step 4. + // The fractional part |nanoseconds / dayLength| is from step 7. // // The denominator for |fractionalWeeks| is |dayLength * abs(oneWeekDays)|. // @@ -4382,236 +3348,259 @@ static bool RoundNumberToIncrement( // = weeks + days / abs(oneWeekDays) + nanoseconds / (dayLength * abs(oneWeekDays)) // = (weeks * dayLength * abs(oneWeekDays) + days * dayLength + nanoseconds) / (dayLength * abs(oneWeekDays)) // + // Because |abs(nanoseconds / dayLength) < 0|, this operation can be rewritten + // to omit the multiplication by |dayLength| when the rounding conditions are + // appropriately modified to account for the |nanoseconds / dayLength| part. + // This allows to implement rounding using only int64 values. + // + // This optimization is currently only implemented when |nanoseconds| is zero. + // + // Example how to expand this optimization for non-zero |nanoseconds|: + // + // |Round(fraction / increment) * increment| with: + // fraction = numerator / denominator + // numerator = weeks * dayLength * abs(oneWeekDays) + days * dayLength + nanoseconds + // denominator = dayLength * abs(oneWeekDays) + // + // When ignoring the |nanoseconds / dayLength| part, this can be simplified to: + // + // |Round(fraction / increment) * increment| with: + // fraction = numerator / denominator + // numerator = weeks * abs(oneWeekDays) + days + // denominator = abs(oneWeekDays) + // + // Where: + // fraction / increment + // = (numerator / denominator) / increment + // = numerator / (denominator * increment) + // + // And |numerator| and |denominator * increment| both fit into int64. + // + // The "ceiling" operation has to be modified from: + // + // CeilDiv(dividend, divisor) + // quot, rem = dividend / divisor + // return quot + (rem > 0) + // + // To: + // + // CeilDiv(dividend, divisor, fractional) + // quot, rem = dividend / divisor + // return quot + ((rem > 0) || (fractional > 0)) + // + // To properly account for the fractional |nanoseconds| part. Alternatively + // |dividend| can be modified before calling `CeilDiv`. + // // clang-format on - do { - auto nanoseconds = nanosAndDays.nanoseconds().toNanoseconds(); - if (!nanoseconds.isValid()) { - break; - } - - auto dayLength = nanosAndDays.dayLength().toNanoseconds(); - if (!dayLength.isValid()) { - break; - } + if (fractionalDays.time == 0) { + auto [numerator, denominator] = fraction; + int64_t totalDays = fractionalDays.days + denominator * numerator; - auto denominator = dayLength * std::abs(oneUnitDays); - if (!denominator.isValid()) { - break; + if (computeRemainder == ComputeRemainder::Yes) { + constexpr auto rounded = Int128{0}; + double total = FractionToDouble(totalDays, denominator); + return {rounded, total}; } - int64_t intDays; - if (!mozilla::NumberEqualsInt64(durationDays, &intDays)) { - break; - } + auto rounded = + RoundNumberToIncrement(totalDays, denominator, increment, roundingMode); + constexpr double total = 0; + return {rounded, total}; + } - auto nanoDays = DaysFrom(nanosAndDays); - if (!nanoDays) { - break; - } + do { + auto dayLength = mozilla::CheckedInt64(fractionalDays.dayLength); - auto totalDays = mozilla::CheckedInt64(intDays); - totalDays += *nanoDays; - totalDays += daysToAdd; - if (!totalDays.isValid()) { + auto denominator = dayLength * fraction.denominator; + if (!denominator.isValid()) { break; } - auto totalNanoseconds = dayLength * totalDays; - if (!totalNanoseconds.isValid()) { + auto amountNanos = denominator * fraction.numerator; + if (!amountNanos.isValid()) { break; } - totalNanoseconds += nanoseconds; + auto totalNanoseconds = dayLength * fractionalDays.days; + totalNanoseconds += fractionalDays.time; + totalNanoseconds += amountNanos; if (!totalNanoseconds.isValid()) { break; } - int64_t intAmount; - if (!mozilla::NumberEqualsInt64(durationAmount, &intAmount)) { - break; - } - - int64_t intAmountPassed; - if (!mozilla::NumberEqualsInt64(amountPassed, &intAmountPassed)) { - break; + if (computeRemainder == ComputeRemainder::Yes) { + constexpr auto rounded = Int128{0}; + double total = + FractionToDouble(totalNanoseconds.value(), denominator.value()); + return {rounded, total}; } - auto totalAmount = mozilla::CheckedInt64(intAmount) + intAmountPassed; - if (!totalAmount.isValid()) { - break; - } + auto rounded = RoundNumberToIncrement( + totalNanoseconds.value(), denominator.value(), increment, roundingMode); + constexpr double total = 0; + return {rounded, total}; + } while (false); - auto amountNanos = denominator * totalAmount; - if (!amountNanos.isValid()) { - break; - } + // Use int128 when values are too large for int64. Additionally assert all + // values fit into int128. - totalNanoseconds += amountNanos; - if (!totalNanoseconds.isValid()) { - break; - } + // `dayLength` < 2**53 + auto dayLength = Int128{fractionalDays.dayLength}; + MOZ_ASSERT(dayLength < Int128{1} << 53); - double rounded; - double total = 0; - if (computeRemainder == ComputeRemainder::No) { - if (!temporal::RoundNumberToIncrement(cx, totalNanoseconds.value(), - denominator.value(), increment, - roundingMode, &rounded)) { - return false; - } - } else { - TruncateNumber(totalNanoseconds.value(), denominator.value(), &rounded, - &total); - } + // `fraction.denominator` < MaxEpochDaysDuration + // log2(MaxEpochDaysDuration) = ~27.57. + auto denominator = dayLength * Int128{fraction.denominator}; + MOZ_ASSERT(denominator < Int128{1} << (53 + 28)); - *result = {rounded, total}; - return true; - } while (false); + // log2(24*60*60) = ~16.4 and log2(2 * MaxEpochDaysDuration) = ~28.57. + // + // `abs(MaxFractionalDays)` + // = `abs(2 * MaxDurationDays + 2 * MaxEpochDaysDuration)` + // = `abs(2 * 2**(53 - 16) + 2 * MaxEpochDaysDuration)` + // ≤ 2 * 2**37 + 2**29 + // ≤ 2**39 + auto totalDays = Int128{fractionalDays.days}; + MOZ_ASSERT(totalDays.abs() <= Uint128{1} << 39); + + // `abs(fraction.numerator)` ≤ (2**33) + auto totalAmount = Int128{fraction.numerator}; + MOZ_ASSERT(totalAmount.abs() <= Uint128{1} << 33); + + // `denominator` < 2**(53 + 28) + // `abs(totalAmount)` <= 2**33 + // + // `denominator * totalAmount` + // ≤ 2**(53 + 28) * 2**33 + // = 2**(53 + 28 + 33) + // = 2**114 + auto amountNanos = denominator * totalAmount; + MOZ_ASSERT(amountNanos.abs() <= Uint128{1} << 114); + + // `dayLength` < 2**53 + // `totalDays` ≤ 2**39 + // `fractionalDays.time` < `dayLength` < 2**53 + // `amountNanos` ≤ 2**114 + // + // `dayLength * totalDays` + // ≤ 2**(53 + 39) = 2**92 + // + // `dayLength * totalDays + fractionalDays.time` + // ≤ 2**93 + // + // `dayLength * totalDays + fractionalDays.time + amountNanos` + // ≤ 2**115 + auto totalNanoseconds = dayLength * totalDays; + totalNanoseconds += Int128{fractionalDays.time}; + totalNanoseconds += amountNanos; + MOZ_ASSERT(totalNanoseconds.abs() <= Uint128{1} << 115); - return RoundNumberToIncrementSlow( - cx, durationAmount, amountPassed, durationDays, daysToAdd, nanosAndDays, - oneUnitDays, increment, roundingMode, computeRemainder, result); -} + if (computeRemainder == ComputeRemainder::Yes) { + constexpr auto rounded = Int128{0}; + double total = FractionToDouble(totalNanoseconds, denominator); + return {rounded, total}; + } -static bool RoundNumberToIncrement( - JSContext* cx, double durationDays, - Handle nanosAndDays, Increment increment, - TemporalRoundingMode roundingMode, ComputeRemainder computeRemainder, - RoundedNumber* result) { - constexpr double daysAmount = 0; - constexpr double daysPassed = 0; - constexpr int32_t oneDayDays = 1; - constexpr int32_t daysToAdd = 0; - - return RoundNumberToIncrement(cx, daysAmount, daysPassed, durationDays, - daysToAdd, nanosAndDays, oneDayDays, increment, - roundingMode, computeRemainder, result); + auto rounded = RoundNumberToIncrement(totalNanoseconds, denominator, + increment, roundingMode); + constexpr double total = 0; + return {rounded, total}; } -static bool RoundDurationYear(JSContext* cx, const Duration& duration, - Handle nanosAndDays, +static bool RoundDurationYear(JSContext* cx, const NormalizedDuration& duration, + FractionalDays fractionalDays, Increment increment, TemporalRoundingMode roundingMode, Handle> dateRelativeTo, Handle calendar, ComputeRemainder computeRemainder, RoundedDuration* result) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; + auto [years, months, weeks, days] = duration.date; - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; - - // Step 10.a. - Duration yearsDuration = {years}; + // Step 9.a. + auto yearsDuration = DateDuration{years}; - // Step 10.b. + // Step 9.b. auto yearsLater = AddDate(cx, calendar, dateRelativeTo, yearsDuration); if (!yearsLater) { return false; } auto yearsLaterDate = ToPlainDate(&yearsLater.unwrap()); - // Step 10.f. (Reordered) + // Step 9.f. (Reordered) Rooted> newRelativeTo(cx, yearsLater); - // Step 10.c. - Duration yearsMonthsWeeks = {years, months, weeks}; + // Step 9.c. + auto yearsMonthsWeeks = DateDuration{years, months, weeks}; - // Step 10.d. + // Step 9.d. PlainDate yearsMonthsWeeksLater; if (!AddDate(cx, calendar, dateRelativeTo, yearsMonthsWeeks, &yearsMonthsWeeksLater)) { return false; } - // Step 10.e. + // Step 9.e. int32_t monthsWeeksInDays = DaysUntil(yearsLaterDate, yearsMonthsWeeksLater); - MOZ_ASSERT(std::abs(monthsWeeksInDays) <= epochDays); + MOZ_ASSERT(std::abs(monthsWeeksInDays) <= MaxEpochDaysDuration); - // Step 10.f. (Moved up) + // Step 9.f. (Moved up) - // Step 10.g. - // Our implementation keeps |days| and |monthsWeeksInDays| separate. + // Step 9.g. + fractionalDays += monthsWeeksInDays; // FIXME: spec issue - truncation doesn't match the spec polyfill. // https://github.com/tc39/proposal-temporal/issues/2540 - // Step 10.h. - double truncatedDays; - if (!TruncateDays(cx, nanosAndDays, days, monthsWeeksInDays, - &truncatedDays)) { - return false; - } - - // FIXME: spec bug - truncated days can be infinity: - // - // Temporal.Duration.from({ - // days: Number.MAX_VALUE, - // hours: Number.MAX_VALUE, - // }).round({ - // smallestUnit: "years", - // relativeTo: "1970-01-01", - // }); - if (!IsInteger(truncatedDays)) { - MOZ_ASSERT(std::isinf(truncatedDays)); - JS_ReportErrorASCII(cx, "truncated days is infinity"); - return false; - } - + // Step 9.h. PlainDate isoResult; - if (!AddISODate(cx, yearsLaterDate, {0, 0, 0, truncatedDays}, - TemporalOverflow::Constrain, &isoResult)) { + if (!BalanceISODate(cx, yearsLaterDate, fractionalDays.truncate(), + &isoResult)) { return false; } - // Step 10.i. + // Step 9.i. Rooted wholeDaysLater( cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); if (!wholeDaysLater) { return false; } - // Steps 10.j-l. - Duration timePassed; + // Steps 9.j-l. + DateDuration timePassed; if (!DifferenceDate(cx, calendar, newRelativeTo, wholeDaysLater, TemporalUnit::Year, &timePassed)) { return false; } - // Step 10.m. - double yearsPassed = timePassed.years; + // Step 9.m. + int64_t yearsPassed = timePassed.years; - // Step 10.n. - // Our implementation keeps |years| and |yearsPassed| separate. + // Step 9.n. + years += yearsPassed; - // Step 10.o. - Duration yearsPassedDuration = {yearsPassed}; + // Step 9.o. + auto yearsPassedDuration = DateDuration{yearsPassed}; - // Steps 10.p-r. + // Steps 9.p-r. int32_t daysPassed; if (!MoveRelativeDate(cx, calendar, newRelativeTo, yearsPassedDuration, &newRelativeTo, &daysPassed)) { return false; } - MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + MOZ_ASSERT(std::abs(daysPassed) <= MaxEpochDaysDuration); - // Step 10.s. - // - // Our implementation keeps |days| and |daysPassed| separate. - int32_t daysToAdd = monthsWeeksInDays - daysPassed; - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); + // Step 9.s. + fractionalDays -= daysPassed; - // Steps 10.t. - double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + // Steps 9.t. + int32_t sign = fractionalDays.sign() < 0 ? -1 : 1; - // Step 10.u. - Duration oneYear = {sign}; + // Step 9.u. + auto oneYear = DateDuration{sign}; - // Steps 10.v-w. + // Steps 9.v-w. Rooted> moveResultIgnored(cx); int32_t oneYearDays; if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneYear, @@ -4619,158 +3608,137 @@ static bool RoundDurationYear(JSContext* cx, const Duration& duration, return false; } - // Step 10.x. + // Step 9.x. if (oneYearDays == 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_NUMBER, "days"); return false; } - // Steps 10.y-aa. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, years, yearsPassed, days, daysToAdd, - nanosAndDays, oneYearDays, increment, - roundingMode, computeRemainder, &rounded)) { - return false; - } - auto [numYears, total] = rounded; + // Steps 9.y. + auto fractionalYears = Fraction{years, std::abs(oneYearDays)}; - // Step 10.ab. - double numMonths = 0; - double numWeeks = 0; + // Steps 9.z-aa. + auto [numYears, total] = + RoundNumberToIncrement(fractionalYears, fractionalDays, increment, + roundingMode, computeRemainder); - // Step 20. - Duration resultDuration = {numYears, numMonths, numWeeks}; + // Step 9.ab. + int64_t numMonths = 0; + int64_t numWeeks = 0; + + // Step 9.ac. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + if (numYears.abs() >= (Uint128{1} << 32)) { + return ThrowInvalidDurationPart(cx, double(numYears), "years", + JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + + auto resultDuration = DateDuration{int64_t(numYears), numMonths, numWeeks}; if (!ThrowIfInvalidDuration(cx, resultDuration)) { return false; } - // Step 21. - *result = {resultDuration, total}; + *result = {{resultDuration, time}, total}; return true; } -static bool RoundDurationMonth( - JSContext* cx, const Duration& duration, - Handle nanosAndDays, Increment increment, - TemporalRoundingMode roundingMode, - Handle> dateRelativeTo, - Handle calendar, ComputeRemainder computeRemainder, - RoundedDuration* result) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; +static bool RoundDurationMonth(JSContext* cx, + const NormalizedDuration& duration, + FractionalDays fractionalDays, + Increment increment, + TemporalRoundingMode roundingMode, + Handle> dateRelativeTo, + Handle calendar, + ComputeRemainder computeRemainder, + RoundedDuration* result) { + auto [years, months, weeks, days] = duration.date; - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; - - // Step 11.a. - Duration yearsMonths = {years, months}; + // Step 10.a. + auto yearsMonths = DateDuration{years, months}; - // Step 11.b. + // Step 10.b. auto yearsMonthsLater = AddDate(cx, calendar, dateRelativeTo, yearsMonths); if (!yearsMonthsLater) { return false; } auto yearsMonthsLaterDate = ToPlainDate(&yearsMonthsLater.unwrap()); - // Step 11.f. (Reordered) + // Step 10.f. (Reordered) Rooted> newRelativeTo(cx, yearsMonthsLater); - // Step 11.c. - Duration yearsMonthsWeeks = {years, months, weeks}; + // Step 10.c. + auto yearsMonthsWeeks = DateDuration{years, months, weeks}; - // Step 11.d. + // Step 10.d. PlainDate yearsMonthsWeeksLater; if (!AddDate(cx, calendar, dateRelativeTo, yearsMonthsWeeks, &yearsMonthsWeeksLater)) { return false; } - // Step 11.e. + // Step 10.e. int32_t weeksInDays = DaysUntil(yearsMonthsLaterDate, yearsMonthsWeeksLater); - MOZ_ASSERT(std::abs(weeksInDays) <= epochDays); + MOZ_ASSERT(std::abs(weeksInDays) <= MaxEpochDaysDuration); - // Step 11.f. (Moved up) + // Step 10.f. (Moved up) - // Step 11.g. - // Our implementation keeps |days| and |weeksInDays| separate. + // Step 10.g. + fractionalDays += weeksInDays; // FIXME: spec issue - truncation doesn't match the spec polyfill. // https://github.com/tc39/proposal-temporal/issues/2540 - // Step 11.h. - double truncatedDays; - if (!TruncateDays(cx, nanosAndDays, days, weeksInDays, &truncatedDays)) { - return false; - } - - // FIXME: spec bug - truncated days can be infinity: - // - // Temporal.Duration.from({ - // days: Number.MAX_VALUE, - // hours: Number.MAX_VALUE, - // }).round({ - // smallestUnit: "months", - // relativeTo: "1970-01-01", - // }); - if (!IsInteger(truncatedDays)) { - MOZ_ASSERT(std::isinf(truncatedDays)); - JS_ReportErrorASCII(cx, "truncated days is infinity"); - return false; - } - + // Step 10.h. PlainDate isoResult; - if (!AddISODate(cx, yearsMonthsLaterDate, {0, 0, 0, truncatedDays}, - TemporalOverflow::Constrain, &isoResult)) { + if (!BalanceISODate(cx, yearsMonthsLaterDate, fractionalDays.truncate(), + &isoResult)) { return false; } - // Step 11.i. + // Step 10.i. Rooted wholeDaysLater( cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); if (!wholeDaysLater) { return false; } - // Steps 11.j-l. - Duration timePassed; + // Steps 10.j-l. + DateDuration timePassed; if (!DifferenceDate(cx, calendar, newRelativeTo, wholeDaysLater, TemporalUnit::Month, &timePassed)) { return false; } - // Step 11.m. - double monthsPassed = timePassed.months; + // Step 10.m. + int64_t monthsPassed = timePassed.months; - // Step 11.n. - // Our implementation keeps |months| and |monthsPassed| separate. + // Step 10.n. + months += monthsPassed; - // Step 11.o. - Duration monthsPassedDuration = {0, monthsPassed}; + // Step 10.o. + auto monthsPassedDuration = DateDuration{0, monthsPassed}; - // Steps 11.p-r. + // Steps 10.p-r. int32_t daysPassed; if (!MoveRelativeDate(cx, calendar, newRelativeTo, monthsPassedDuration, &newRelativeTo, &daysPassed)) { return false; } - MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + MOZ_ASSERT(std::abs(daysPassed) <= MaxEpochDaysDuration); - // Step 11.s. - // - // Our implementation keeps |days| and |daysPassed| separate. - int32_t daysToAdd = weeksInDays - daysPassed; - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); + // Step 10.s. + fractionalDays -= daysPassed; - // Steps 11.t. - double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + // Steps 10.t. + int32_t sign = fractionalDays.sign() < 0 ? -1 : 1; - // Step 11.u. - Duration oneMonth = {0, sign}; + // Step 10.u. + auto oneMonth = DateDuration{0, sign}; - // Steps 11.v-w. + // Steps 10.v-w. Rooted> moveResultIgnored(cx); int32_t oneMonthDays; if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneMonth, @@ -4778,51 +3746,51 @@ static bool RoundDurationMonth( return false; } - // Step 11.x. + // Step 10.x. if (oneMonthDays == 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_NUMBER, "days"); return false; } - // Steps 11.y-aa. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, months, monthsPassed, days, daysToAdd, - nanosAndDays, oneMonthDays, increment, - roundingMode, computeRemainder, &rounded)) { - return false; - } - auto [numMonths, total] = rounded; + // Step 10.y. + auto fractionalMonths = Fraction{months, std::abs(oneMonthDays)}; - // Step 11.ab. - double numWeeks = 0; + // Steps 10.z-aa. + auto [numMonths, total] = + RoundNumberToIncrement(fractionalMonths, fractionalDays, increment, + roundingMode, computeRemainder); - // Step 20. - Duration resultDuration = {years, numMonths, numWeeks}; + // Step 10.ab. + int64_t numWeeks = 0; + + // Step 10.ac. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + if (numMonths.abs() >= (Uint128{1} << 32)) { + return ThrowInvalidDurationPart(cx, double(numMonths), "months", + JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + + auto resultDuration = DateDuration{years, int64_t(numMonths), numWeeks}; if (!ThrowIfInvalidDuration(cx, resultDuration)) { return false; } - // Step 21. - *result = {resultDuration, total}; + *result = {{resultDuration, time}, total}; return true; } -static bool RoundDurationWeek(JSContext* cx, const Duration& duration, - Handle nanosAndDays, +static bool RoundDurationWeek(JSContext* cx, const NormalizedDuration& duration, + FractionalDays fractionalDays, Increment increment, TemporalRoundingMode roundingMode, Handle> dateRelativeTo, Handle calendar, ComputeRemainder computeRemainder, RoundedDuration* result) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; - - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + auto [years, months, weeks, days] = duration.date; auto* unwrappedRelativeTo = dateRelativeTo.unwrap(cx); if (!unwrappedRelativeTo) { @@ -4830,150 +3798,213 @@ static bool RoundDurationWeek(JSContext* cx, const Duration& duration, } auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 12.a - double truncatedDays; - if (!TruncateDays(cx, nanosAndDays, days, 0, &truncatedDays)) { - return false; - } - - // FIXME: spec bug - truncated days can be infinity: - // - // Temporal.Duration.from({ - // days: Number.MAX_VALUE, - // hours: Number.MAX_VALUE, - // }).round({ - // smallestUnit: "weeks", - // relativeTo: "1970-01-01", - // }); - if (!IsInteger(truncatedDays)) { - MOZ_ASSERT(std::isinf(truncatedDays)); - JS_ReportErrorASCII(cx, "truncated days is infinity"); - return false; - } - + // Step 11.a PlainDate isoResult; - if (!AddISODate(cx, relativeToDate, {0, 0, 0, truncatedDays}, - TemporalOverflow::Constrain, &isoResult)) { + if (!BalanceISODate(cx, relativeToDate, fractionalDays.truncate(), + &isoResult)) { return false; } - // Step 12.b. + // Step 11.b. Rooted wholeDaysLater( cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); if (!wholeDaysLater) { return false; } - // Steps 12.c-e. - Duration timePassed; + // Steps 11.c-e. + DateDuration timePassed; if (!DifferenceDate(cx, calendar, dateRelativeTo, wholeDaysLater, TemporalUnit::Week, &timePassed)) { return false; } - // Step 12.f. - double weeksPassed = timePassed.weeks; + // Step 11.f. + int64_t weeksPassed = timePassed.weeks; + + // Step 11.g. + weeks += weeksPassed; + + // Step 11.h. + auto weeksPassedDuration = DateDuration{0, 0, weeksPassed}; + + // Steps 11.i-k. + Rooted> newRelativeTo(cx); + int32_t daysPassed; + if (!MoveRelativeDate(cx, calendar, dateRelativeTo, weeksPassedDuration, + &newRelativeTo, &daysPassed)) { + return false; + } + MOZ_ASSERT(std::abs(daysPassed) <= MaxEpochDaysDuration); + + // Step 11.l. + fractionalDays -= daysPassed; + + // Steps 11.m. + int32_t sign = fractionalDays.sign() < 0 ? -1 : 1; + + // Step 11.n. + auto oneWeek = DateDuration{0, 0, sign}; + + // Steps 11.o-p. + Rooted> moveResultIgnored(cx); + int32_t oneWeekDays; + if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneWeek, + &moveResultIgnored, &oneWeekDays)) { + return false; + } + + // Step 11.q. + if (oneWeekDays == 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_NUMBER, "days"); + return false; + } + + // Step 11.r. + auto fractionalWeeks = Fraction{weeks, std::abs(oneWeekDays)}; + + // Steps 11.s-t. + auto [numWeeks, total] = + RoundNumberToIncrement(fractionalWeeks, fractionalDays, increment, + roundingMode, computeRemainder); + + // Step 11.u. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + if (numWeeks.abs() >= (Uint128{1} << 32)) { + return ThrowInvalidDurationPart(cx, double(numWeeks), "weeks", + JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + + auto resultDuration = DateDuration{years, months, int64_t(numWeeks)}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { + return false; + } + + *result = {{resultDuration, time}, total}; + return true; +} + +static bool RoundDurationDay(JSContext* cx, const NormalizedDuration& duration, + const FractionalDays& fractionalDays, + Increment increment, + TemporalRoundingMode roundingMode, + ComputeRemainder computeRemainder, + RoundedDuration* result) { + auto [years, months, weeks, days] = duration.date; + + // Pass zero fraction. + constexpr auto zero = Fraction{0, 1}; + + // Steps 12.a-b. + auto [numDays, total] = RoundNumberToIncrement( + zero, fractionalDays, increment, roundingMode, computeRemainder); + + MOZ_ASSERT(Int128{INT64_MIN} <= numDays && numDays <= Int128{INT64_MAX}, + "rounded days fits in int64"); + + // Step 12.c. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + auto resultDuration = DateDuration{years, months, weeks, int64_t(numDays)}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { + return false; + } + + *result = {{resultDuration, time}, total}; + return true; +} + +/** + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) + */ +static bool RoundDuration(JSContext* cx, const NormalizedDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + ComputeRemainder computeRemainder, + RoundedDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration.time)); + MOZ_ASSERT_IF(unit > TemporalUnit::Day, IsValidDuration(duration.date)); - // Step 12.g. - // Our implementation keeps |weeks| and |weeksPassed| separate. + // The remainder is only needed when called from |Duration_total|. And `total` + // always passes |increment=1| and |roundingMode=trunc|. + MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, + increment == Increment{1}); + MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, + roundingMode == TemporalRoundingMode::Trunc); - // Step 12.h. - Duration weeksPassedDuration = {0, 0, weeksPassed}; + // Steps 1-5. (Not applicable.) - // Steps 12.i-k. - Rooted> newRelativeTo(cx); - int32_t daysPassed; - if (!MoveRelativeDate(cx, calendar, dateRelativeTo, weeksPassedDuration, - &newRelativeTo, &daysPassed)) { + // Step 6. + if (unit <= TemporalUnit::Week) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, + "relativeTo"); return false; } - MOZ_ASSERT(std::abs(daysPassed) <= epochDays); - // Step 12.l. + // TODO: We could directly return here if unit=nanoseconds and increment=1, + // because in that case this operation is a no-op. This case happens for + // example when calling Temporal.PlainTime.prototype.{since,until} without an + // options object. // - // Our implementation keeps |days| and |daysPassed| separate. - int32_t daysToAdd = -daysPassed; - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays); - - // Steps 12.m. - double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + // But maybe this can be even more efficiently handled in the callers. For + // example when Temporal.PlainTime.prototype.{since,until} is called without + // an options object, we can not only skip the RoundDuration call, but also + // the following BalanceTimeDuration call. - // Step 12.n. - Duration oneWeek = {0, 0, sign}; + // Step 7. (Moved below.) - // Steps 12.o-p. - Rooted> moveResultIgnored(cx); - int32_t oneWeekDays; - if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneWeek, - &moveResultIgnored, &oneWeekDays)) { - return false; - } + // Step 8. (Not applicable.) - // Step 12.q. - if (oneWeekDays == 0) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_INVALID_NUMBER, "days"); - return false; - } + // Steps 9-11. (Not applicable.) - // Steps 12.r-t. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, weeks, weeksPassed, days, daysToAdd, - nanosAndDays, oneWeekDays, increment, - roundingMode, computeRemainder, &rounded)) { - return false; - } - auto [numWeeks, total] = rounded; + // Step 12. + if (unit == TemporalUnit::Day) { + // Step 7. + auto timeAndDays = NormalizedTimeDurationToDays(duration.time); + auto fractionalDays = FractionalDays{duration.date.days, timeAndDays}; - // Step 20. - Duration resultDuration = {years, months, numWeeks}; - if (!ThrowIfInvalidDuration(cx, resultDuration)) { - return false; + return RoundDurationDay(cx, duration, fractionalDays, increment, + roundingMode, computeRemainder, result); } - // Step 21. - *result = {resultDuration, total}; - return true; -} - -static bool RoundDurationDay(JSContext* cx, const Duration& duration, - Handle nanosAndDays, - Increment increment, - TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, - RoundedDuration* result) { - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + MOZ_ASSERT(TemporalUnit::Hour <= unit && unit <= TemporalUnit::Nanosecond); - // Steps 13.a-b. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, days, nanosAndDays, increment, roundingMode, - computeRemainder, &rounded)) { - return false; - } - auto [numDays, total] = rounded; + // Steps 13-18. + auto time = duration.time; + double total = 0; + if (computeRemainder == ComputeRemainder::No) { + if (!RoundNormalizedTimeDurationToIncrement(cx, time, unit, increment, + roundingMode, &time)) { + return false; + } + } else { + MOZ_ASSERT(increment == Increment{1}); + MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); - // Step 20. - Duration resultDuration = {years, months, weeks, numDays}; - if (!ThrowIfInvalidDuration(cx, resultDuration)) { - return false; + total = TotalNormalizedTimeDuration(duration.time, unit); } + MOZ_ASSERT(IsValidNormalizedTimeDuration(time)); - // Step 21. - *result = {resultDuration, total}; + // Step 19. + MOZ_ASSERT(IsValidDuration(duration.date)); + *result = {{duration.date, time}, total}; return true; } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ static bool RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, Handle> plainRelativeTo, Handle calendar, Handle zonedRelativeTo, @@ -4982,9 +4013,11 @@ static bool RoundDuration( ComputeRemainder computeRemainder, RoundedDuration* result) { // Note: |duration.days| can have a different sign than the other date // components. The date and time components can have different signs, too. - MOZ_ASSERT( - IsValidDuration({duration.years, duration.months, duration.weeks})); - MOZ_ASSERT(IsValidDuration(duration.time())); + MOZ_ASSERT(IsValidDuration(Duration{double(duration.date.years), + double(duration.date.months), + double(duration.date.weeks)})); + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration.time)); + MOZ_ASSERT_IF(unit > TemporalUnit::Day, IsValidDuration(duration.date)); MOZ_ASSERT(plainRelativeTo || zonedRelativeTo, "Use RoundDuration without relativeTo when plainRelativeTo and " @@ -5040,69 +4073,61 @@ static bool RoundDuration( MOZ_ASSERT(TemporalUnit::Year <= unit && unit <= TemporalUnit::Day); // Steps 7.a-c. - Rooted nanosAndDays(cx); + FractionalDays fractionalDays; if (zonedRelativeTo) { - // Step 7.b.i. (Reordered) + // Step 7.a.i. Rooted intermediate(cx); if (!MoveRelativeZonedDateTime(cx, zonedRelativeTo, calendar, timeZone, - duration.date(), precalculatedPlainDateTime, + duration.date, precalculatedPlainDateTime, &intermediate)) { return false; } - // Steps 7.a and 7.b.ii. - if (!NanosecondsToDays(cx, duration, intermediate, timeZone, - &nanosAndDays)) { + // Steps 7.a.ii. + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, duration.time, intermediate, timeZone, + &timeAndDays)) { return false; } - // Step 7.b.iii. (Not applicable in our implementation.) + // Step 7.a.iii. + fractionalDays = FractionalDays{duration.date.days, timeAndDays}; } else { - // Steps 7.a and 7.c. - if (!::NanosecondsToDays(cx, duration, &nanosAndDays)) { - return false; - } + // Step 7.b. + auto timeAndDays = NormalizedTimeDurationToDays(duration.time); + fractionalDays = FractionalDays{duration.date.days, timeAndDays}; } - // NanosecondsToDays guarantees that |abs(nanosAndDays.nanoseconds)| is less - // than |abs(nanosAndDays.dayLength)|. - MOZ_ASSERT(nanosAndDays.nanoseconds().abs() < nanosAndDays.dayLength()); - - // Step 7.d. (Moved below) - - // Step 7.e. (Implicit) + // Step 7.c. (Moved below) // Step 8. (Not applicable) - // Step 9. - // FIXME: spec issue - `total` doesn't need be initialised. - - // Steps 10-21. + // Steps 9-19. switch (unit) { - // Steps 10 and 20-21. + // Steps 9 and 19. case TemporalUnit::Year: - return RoundDurationYear(cx, duration, nanosAndDays, increment, + return RoundDurationYear(cx, duration, fractionalDays, increment, roundingMode, plainRelativeTo, calendar, computeRemainder, result); - // Steps 11 and 20-21. + // Steps 10 and 19. case TemporalUnit::Month: - return RoundDurationMonth(cx, duration, nanosAndDays, increment, + return RoundDurationMonth(cx, duration, fractionalDays, increment, roundingMode, plainRelativeTo, calendar, computeRemainder, result); - // Steps 12 and 20-21. + // Steps 11 and 19. case TemporalUnit::Week: - return RoundDurationWeek(cx, duration, nanosAndDays, increment, + return RoundDurationWeek(cx, duration, fractionalDays, increment, roundingMode, plainRelativeTo, calendar, computeRemainder, result); - // Steps 13 and 20-21. + // Steps 12 and 19. case TemporalUnit::Day: - return RoundDurationDay(cx, duration, nanosAndDays, increment, + return RoundDurationDay(cx, duration, fractionalDays, increment, roundingMode, computeRemainder, result); - // Steps 14-19. (Handled elsewhere) + // Steps 13-18. (Handled elsewhere) case TemporalUnit::Auto: case TemporalUnit::Hour: case TemporalUnit::Minute: @@ -5117,55 +4142,51 @@ static bool RoundDuration( } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -static bool RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, +bool js::temporal::RoundDuration( + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, Handle> plainRelativeTo, - Handle calendar, Handle zonedRelativeTo, - Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - double* result) { - // Only called from |Duration_total|, which always passes |increment=1| and - // |roundingMode=trunc|. - MOZ_ASSERT(increment == Increment{1}); - MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); + Handle calendar, NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); + Rooted zonedRelativeTo(cx, ZonedDateTime{}); + Rooted timeZone(cx, TimeZoneRecord{}); + mozilla::Maybe precalculatedPlainDateTime{}; RoundedDuration rounded; if (!::RoundDuration(cx, duration, increment, unit, roundingMode, plainRelativeTo, calendar, zonedRelativeTo, timeZone, - precalculatedPlainDateTime, ComputeRemainder::Yes, + precalculatedPlainDateTime, ComputeRemainder::No, &rounded)) { return false; } - *result = rounded.total; + *result = rounded.duration; return true; } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -static bool RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, +bool js::temporal::RoundDuration( + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle> plainRelativeTo, - Handle calendar, Handle zonedRelativeTo, - Handle timeZone, - mozilla::Maybe precalculatedPlainDateTime, - Duration* result) { + Handle plainRelativeTo, Handle calendar, + Handle zonedRelativeTo, Handle timeZone, + const PlainDateTime& precalculatedPlainDateTime, + NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); + RoundedDuration rounded; if (!::RoundDuration(cx, duration, increment, unit, roundingMode, plainRelativeTo, calendar, zonedRelativeTo, timeZone, - precalculatedPlainDateTime, ComputeRemainder::No, - &rounded)) { + mozilla::SomeRef(precalculatedPlainDateTime), + ComputeRemainder::No, &rounded)) { return false; } @@ -5173,46 +4194,6 @@ static bool RoundDuration( return true; } -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -bool js::temporal::RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle> plainRelativeTo, - Handle calendar, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - Rooted zonedRelativeTo(cx, ZonedDateTime{}); - Rooted timeZone(cx, TimeZoneRecord{}); - mozilla::Maybe precalculatedPlainDateTime{}; - return ::RoundDuration(cx, duration, increment, unit, roundingMode, - plainRelativeTo, calendar, zonedRelativeTo, timeZone, - precalculatedPlainDateTime, result); -} - -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -bool js::temporal::RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle plainRelativeTo, Handle calendar, - Handle zonedRelativeTo, Handle timeZone, - const PlainDateTime& precalculatedPlainDateTime, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - return ::RoundDuration(cx, duration, increment, unit, roundingMode, - plainRelativeTo, calendar, zonedRelativeTo, timeZone, - mozilla::SomeRef(precalculatedPlainDateTime), result); -} - enum class DurationOperation { Add, Subtract }; /** @@ -5441,40 +4422,32 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { return false; } - Rooted> plainRelativeTo(cx); - Rooted zonedRelativeTo(cx); - Rooted timeZone(cx); + // Step 3. + Rooted options(cx); if (args.hasDefined(2)) { - // Step 3. - Rooted options( - cx, RequireObjectArg(cx, "options", "compare", args[2])); + options = RequireObjectArg(cx, "options", "compare", args[2]); if (!options) { return false; } + } - // Step 4. - if (one == two) { - args.rval().setInt32(0); - return true; - } + // Step 4. + if (one == two) { + args.rval().setInt32(0); + return true; + } - // Steps 5-8. + // Steps 5-8. + Rooted> plainRelativeTo(cx); + Rooted zonedRelativeTo(cx); + Rooted timeZone(cx); + if (options) { if (!ToRelativeTemporalObject(cx, options, &plainRelativeTo, &zonedRelativeTo, &timeZone)) { return false; } MOZ_ASSERT(!plainRelativeTo || !zonedRelativeTo); MOZ_ASSERT_IF(zonedRelativeTo, timeZone.receiver()); - } else { - // Step 3. (Not applicable in our implementation.) - - // Step 4. - if (one == two) { - args.rval().setInt32(0); - return true; - } - - // Steps 5-8. (Not applicable in our implementation.) } // Steps 9-10. @@ -5498,7 +4471,7 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { if (zonedRelativeTo && (calendarUnitsPresent || one.days != 0 || two.days != 0)) { // Step 12.a. - auto instant = zonedRelativeTo.instant(); + const auto& instant = zonedRelativeTo.instant(); // Step 12.b. PlainDateTime dateTime; @@ -5507,59 +4480,65 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { } // Step 12.c. + auto normalized1 = CreateNormalizedDurationRecord(one); + + // Step 12.d. + auto normalized2 = CreateNormalizedDurationRecord(two); + + // Step 12.e. Instant after1; - if (!AddZonedDateTime(cx, instant, timeZone, calendar, one, dateTime, - &after1)) { + if (!AddZonedDateTime(cx, instant, timeZone, calendar, normalized1, + dateTime, &after1)) { return false; } - // Step 12.d. + // Step 12.f. Instant after2; - if (!AddZonedDateTime(cx, instant, timeZone, calendar, two, dateTime, - &after2)) { + if (!AddZonedDateTime(cx, instant, timeZone, calendar, normalized2, + dateTime, &after2)) { return false; } - // Steps 12.e-g. + // Steps 12.g-i. args.rval().setInt32(after1 < after2 ? -1 : after1 > after2 ? 1 : 0); return true; } // Steps 13-14. - double days1, days2; + int64_t days1, days2; if (calendarUnitsPresent) { // FIXME: spec issue - directly throw an error if plainRelativeTo is undef. // Step 13.a. DateDuration unbalanceResult1; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, one, TemporalUnit::Day, - plainRelativeTo, calendar, - &unbalanceResult1)) { + if (!UnbalanceDateDurationRelative(cx, one.toDateDuration(), + TemporalUnit::Day, plainRelativeTo, + calendar, &unbalanceResult1)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, one, TemporalUnit::Day, - &unbalanceResult1)) { + if (!UnbalanceDateDurationRelative( + cx, one.toDateDuration(), TemporalUnit::Day, &unbalanceResult1)) { return false; } - MOZ_ASSERT(one.date() == unbalanceResult1.toDuration()); + MOZ_ASSERT(one.toDateDuration() == unbalanceResult1); } // Step 13.b. DateDuration unbalanceResult2; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, two, TemporalUnit::Day, - plainRelativeTo, calendar, - &unbalanceResult2)) { + if (!UnbalanceDateDurationRelative(cx, two.toDateDuration(), + TemporalUnit::Day, plainRelativeTo, + calendar, &unbalanceResult2)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, two, TemporalUnit::Day, - &unbalanceResult2)) { + if (!UnbalanceDateDurationRelative( + cx, two.toDateDuration(), TemporalUnit::Day, &unbalanceResult2)) { return false; } - MOZ_ASSERT(two.date() == unbalanceResult2.toDuration()); + MOZ_ASSERT(two.toDateDuration() == unbalanceResult2); } // Step 13.c. @@ -5569,74 +4548,32 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { days2 = unbalanceResult2.days; } else { // Step 14.a. - days1 = one.days; + days1 = mozilla::AssertedCast(one.days); // Step 14.b. - days2 = two.days; + days2 = mozilla::AssertedCast(two.days); } - // Note: duration units can be arbitrary doubles, so we need to use BigInts - // Test case: - // - // Temporal.Duration.compare({ - // milliseconds: 10000000000000, microseconds: 4, nanoseconds: 95 - // }, { - // nanoseconds:10000000000000004000 - // }) - // - // This must return -1, but would return 0 when |double| is used. - // - // Note: BigInt(10000000000000004000) is 10000000000000004096n - - Duration oneTotal = { - 0, - 0, - 0, - days1, - one.hours, - one.minutes, - one.seconds, - one.milliseconds, - one.microseconds, - one.nanoseconds, - }; - Duration twoTotal = { - 0, - 0, - 0, - days2, - two.hours, - two.minutes, - two.seconds, - two.milliseconds, - two.microseconds, - two.nanoseconds, - }; - - // Steps 15-21. - // - // Fast path when the total duration amount fits into an int64. - if (auto ns1 = TotalDurationNanoseconds(oneTotal)) { - if (auto ns2 = TotalDurationNanoseconds(twoTotal)) { - args.rval().setInt32(*ns1 < *ns2 ? -1 : *ns1 > *ns2 ? 1 : 0); - return true; - } - } + // Step 15. + auto normalized1 = NormalizeTimeDuration(one); - // Steps 15 and 17. - Rooted ns1(cx, TotalDurationNanosecondsSlow(cx, oneTotal)); - if (!ns1) { + // Step 16. + if (!Add24HourDaysToNormalizedTimeDuration(cx, normalized1, days1, + &normalized1)) { return false; } - // Steps 16 and 18. - auto* ns2 = TotalDurationNanosecondsSlow(cx, twoTotal); - if (!ns2) { + // Step 17. + auto normalized2 = NormalizeTimeDuration(two); + + // Step 18. + if (!Add24HourDaysToNormalizedTimeDuration(cx, normalized2, days2, + &normalized2)) { return false; } - // Step 19-21. - args.rval().setInt32(BigInt::compare(ns1, ns2)); + // Step 19. + args.rval().setInt32(CompareNormalizedTimeDuration(normalized1, normalized2)); return true; } @@ -5834,10 +4771,10 @@ static bool Duration_nanoseconds(JSContext* cx, unsigned argc, Value* vp) { * get Temporal.Duration.prototype.sign */ static bool Duration_sign(JSContext* cx, const CallArgs& args) { + auto duration = ToDuration(&args.thisv().toObject().as()); + // Step 3. - auto* duration = &args.thisv().toObject().as(); - int32_t sign = DurationSign(ToDuration(duration)); - args.rval().setInt32(sign); + args.rval().setInt32(DurationSign(duration)); return true; } @@ -5854,12 +4791,10 @@ static bool Duration_sign(JSContext* cx, unsigned argc, Value* vp) { * get Temporal.Duration.prototype.blank */ static bool Duration_blank(JSContext* cx, const CallArgs& args) { - // Step 3. - auto* duration = &args.thisv().toObject().as(); - int32_t sign = DurationSign(ToDuration(duration)); + auto duration = ToDuration(&args.thisv().toObject().as()); - // Steps 4-5. - args.rval().setBoolean(sign == 0); + // Steps 3-5. + args.rval().setBoolean(duration == Duration{}); return true; } @@ -5878,10 +4813,8 @@ static bool Duration_blank(JSContext* cx, unsigned argc, Value* vp) { * ToPartialDuration ( temporalDurationLike ) */ static bool Duration_with(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as(); - // Absent values default to the corresponding values of |this| object. - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as()); // Steps 3-23. Rooted temporalDurationLike( @@ -5916,8 +4849,7 @@ static bool Duration_with(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.negated ( ) */ static bool Duration_negated(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as(); - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as()); // Step 3. auto* result = CreateTemporalDuration(cx, duration.negate()); @@ -5942,8 +4874,7 @@ static bool Duration_negated(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.abs ( ) */ static bool Duration_abs(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as(); - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as()); // Step 3. auto* result = CreateTemporalDuration(cx, AbsoluteDuration(duration)); @@ -6002,8 +4933,7 @@ static bool Duration_subtract(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.round ( roundTo ) */ static bool Duration_round(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as(); - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as()); // Step 18. (Reordered) auto existingLargestUnit = DefaultTemporalLargestUnit(duration); @@ -6208,7 +5138,7 @@ static bool Duration_round(JSContext* cx, const CallArgs& args) { PlainDateTime relativeToDateTime; if (zonedRelativeTo && plainDateTimeOrRelativeToWillBeUsed) { // Steps 34.a-b. - auto instant = zonedRelativeTo.instant(); + const auto& instant = zonedRelativeTo.instant(); // Step 34.c. if (!GetPlainDateTimeFor(cx, timeZone, instant, &relativeToDateTime)) { @@ -6240,46 +5170,48 @@ static bool Duration_round(JSContext* cx, const CallArgs& args) { // Step 36. DateDuration unbalanceResult; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, duration, largestUnit, - plainRelativeTo, calendar, + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), + largestUnit, plainRelativeTo, calendar, &unbalanceResult)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, duration, largestUnit, - &unbalanceResult)) { + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), + largestUnit, &unbalanceResult)) { return false; } - MOZ_ASSERT(duration.date() == unbalanceResult.toDuration()); + MOZ_ASSERT(duration.toDateDuration() == unbalanceResult); } + MOZ_ASSERT(IsValidDuration(unbalanceResult)); // Steps 37-38. - Duration roundInput = { - unbalanceResult.years, unbalanceResult.months, unbalanceResult.weeks, - unbalanceResult.days, duration.hours, duration.minutes, - duration.seconds, duration.milliseconds, duration.microseconds, - duration.nanoseconds, - }; - Duration roundResult; + auto roundInput = + NormalizedDuration{unbalanceResult, NormalizeTimeDuration(duration)}; + RoundedDuration rounded; if (plainRelativeTo || zonedRelativeTo) { if (!::RoundDuration(cx, roundInput, roundingIncrement, smallestUnit, roundingMode, plainRelativeTo, calendar, zonedRelativeTo, timeZone, precalculatedPlainDateTime, - &roundResult)) { + ComputeRemainder::No, &rounded)) { return false; } } else { + MOZ_ASSERT(IsValidDuration(roundInput)); + if (!::RoundDuration(cx, roundInput, roundingIncrement, smallestUnit, - roundingMode, &roundResult)) { + roundingMode, ComputeRemainder::No, &rounded)) { return false; } } - // Steps 39-40. + // Step 39. + auto roundResult = rounded.duration; + + // Steps 40-41. TimeDuration balanceResult; if (zonedRelativeTo) { - // Step 39.a. - Duration adjustResult; + // Step 40.a. + NormalizedDuration adjustResult; if (!AdjustRoundedDurationDays(cx, roundResult, roundingIncrement, smallestUnit, roundingMode, zonedRelativeTo, calendar, timeZone, @@ -6288,46 +5220,51 @@ static bool Duration_round(JSContext* cx, const CallArgs& args) { } roundResult = adjustResult; - // Step 39.b. + // Step 40.b. if (!BalanceTimeDurationRelative( cx, roundResult, largestUnit, zonedRelativeTo, timeZone, precalculatedPlainDateTime, &balanceResult)) { return false; } } else { - // Step 40.a. - if (!BalanceTimeDuration(cx, roundResult, largestUnit, &balanceResult)) { + // Step 41.a. + NormalizedTimeDuration withDays; + if (!Add24HourDaysToNormalizedTimeDuration( + cx, roundResult.time, roundResult.date.days, &withDays)) { + return false; + } + + // Step 41.b. + if (!temporal::BalanceTimeDuration(cx, withDays, largestUnit, + &balanceResult)) { return false; } } - // Step 41. - Duration balanceInput = { - roundResult.years, - roundResult.months, - roundResult.weeks, + // Step 42. + auto balanceInput = DateDuration{ + roundResult.date.years, + roundResult.date.months, + roundResult.date.weeks, balanceResult.days, }; - DateDuration result; + DateDuration dateResult; if (!::BalanceDateDurationRelative(cx, balanceInput, largestUnit, smallestUnit, plainRelativeTo, calendar, - &result)) { + &dateResult)) { return false; } - // Step 42. - auto* obj = CreateTemporalDuration(cx, { - result.years, - result.months, - result.weeks, - result.days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, - }); + // Step 43. + auto result = Duration{ + double(dateResult.years), double(dateResult.months), + double(dateResult.weeks), double(dateResult.days), + double(balanceResult.hours), double(balanceResult.minutes), + double(balanceResult.seconds), double(balanceResult.milliseconds), + balanceResult.microseconds, balanceResult.nanoseconds, + }; + + auto* obj = CreateTemporalDuration(cx, result); if (!obj) { return false; } @@ -6404,14 +5341,13 @@ static bool Duration_total(JSContext* cx, const CallArgs& args) { // Step 13. bool plainDateTimeOrRelativeToWillBeUsed = - unit <= TemporalUnit::Day || duration.years != 0 || - duration.months != 0 || duration.weeks != 0 || duration.days != 0; + unit <= TemporalUnit::Day || duration.toDateDuration() != DateDuration{}; // Step 14. PlainDateTime relativeToDateTime; if (zonedRelativeTo && plainDateTimeOrRelativeToWillBeUsed) { // Steps 14.a-b. - auto instant = zonedRelativeTo.instant(); + const auto& instant = zonedRelativeTo.instant(); // Step 14.c. if (!GetPlainDateTimeFor(cx, timeZone, instant, &relativeToDateTime)) { @@ -6443,34 +5379,27 @@ static bool Duration_total(JSContext* cx, const CallArgs& args) { // Step 16. DateDuration unbalanceResult; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, duration, unit, plainRelativeTo, - calendar, &unbalanceResult)) { + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), unit, + plainRelativeTo, calendar, + &unbalanceResult)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, duration, unit, &unbalanceResult)) { + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), unit, + &unbalanceResult)) { return false; } - MOZ_ASSERT(duration.date() == unbalanceResult.toDuration()); + MOZ_ASSERT(duration.toDateDuration() == unbalanceResult); } - Duration balanceInput = { - 0, - 0, - 0, - unbalanceResult.days, - duration.hours, - duration.minutes, - duration.seconds, - duration.milliseconds, - duration.microseconds, - duration.nanoseconds, - }; + // Step 17. + int64_t unbalancedDays = unbalanceResult.days; - // Steps 17-18. - TimeDuration balanceResult; + // Steps 18-19. + int64_t days; + NormalizedTimeDuration normTime; if (zonedRelativeTo) { - // Step 17.a + // Step 18.a Rooted intermediate(cx); if (!MoveRelativeZonedDateTime( cx, zonedRelativeTo, calendar, timeZone, @@ -6480,63 +5409,131 @@ static bool Duration_total(JSContext* cx, const CallArgs& args) { return false; } - // Step 17.b. - if (!BalancePossiblyInfiniteTimeDurationRelative( - cx, balanceInput, unit, intermediate, timeZone, &balanceResult)) { + // Step 18.b. + auto timeDuration = NormalizeTimeDuration(duration); + + // Step 18.c + const auto& startNs = intermediate.instant(); + + // Step 18.d. + const auto& startInstant = startNs; + + // Step 18.e. + mozilla::Maybe startDateTime{}; + + // Steps 18.f-g. + Instant intermediateNs; + if (unbalancedDays != 0) { + // Step 18.f.i. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &dateTime)) { + return false; + } + startDateTime = mozilla::Some(dateTime); + + // Step 18.f.ii. + Rooted isoCalendar(cx, CalendarValue(cx->names().iso8601)); + Instant addResult; + if (!AddDaysToZonedDateTime(cx, startInstant, dateTime, timeZone, + isoCalendar, unbalancedDays, &addResult)) { + return false; + } + + // Step 18.f.iii. + intermediateNs = addResult; + } else { + // Step 18.g. + intermediateNs = startNs; + } + + // Step 18.h. + Instant endNs; + if (!AddInstant(cx, intermediateNs, timeDuration, &endNs)) { return false; } + + // Step 18.i. + auto difference = + NormalizedTimeDurationFromEpochNanosecondsDifference(endNs, startNs); + + // Steps 18.j-k. + // + // Avoid calling NormalizedTimeDurationToDays for a zero time difference. + if (TemporalUnit::Year <= unit && unit <= TemporalUnit::Day && + difference != NormalizedTimeDuration{}) { + // Step 18.j.i. + if (!startDateTime) { + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &dateTime)) { + return false; + } + startDateTime = mozilla::Some(dateTime); + } + + // Step 18.j.ii. + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, difference, intermediate, timeZone, + *startDateTime, &timeAndDays)) { + return false; + } + + // Step 18.j.iii. + normTime = NormalizedTimeDuration::fromNanoseconds(timeAndDays.time); + + // Step 18.j.iv. + days = timeAndDays.days; + } else { + // Step 18.k.i. + normTime = difference; + days = 0; + } } else { - // Step 18. - if (!BalancePossiblyInfiniteTimeDuration(cx, balanceInput, unit, - &balanceResult)) { + // Step 19.a. + auto timeDuration = NormalizeTimeDuration(duration); + + // Step 19.b. + if (!Add24HourDaysToNormalizedTimeDuration(cx, timeDuration, unbalancedDays, + &normTime)) { return false; } - } - // Steps 19-20. - for (double v : { - balanceResult.days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, - }) { - if (std::isinf(v)) { - args.rval().setDouble(v); - return true; - } + // Step 19.c. + days = 0; } - MOZ_ASSERT(IsValidDuration(balanceResult.toDuration())); + MOZ_ASSERT(IsValidNormalizedTimeDuration(normTime)); - // Step 21. (Not applicable in our implementation.) - - // Step 22. - Duration roundInput = { - unbalanceResult.years, unbalanceResult.months, - unbalanceResult.weeks, balanceResult.days, - balanceResult.hours, balanceResult.minutes, - balanceResult.seconds, balanceResult.milliseconds, - balanceResult.microseconds, balanceResult.nanoseconds, + // Step 20. + auto roundInput = NormalizedDuration{ + { + unbalanceResult.years, + unbalanceResult.months, + unbalanceResult.weeks, + days, + }, + normTime, }; - double total; + MOZ_ASSERT_IF(unit > TemporalUnit::Day, IsValidDuration(roundInput.date)); + + RoundedDuration rounded; if (plainRelativeTo || zonedRelativeTo) { if (!::RoundDuration(cx, roundInput, Increment{1}, unit, TemporalRoundingMode::Trunc, plainRelativeTo, calendar, zonedRelativeTo, timeZone, precalculatedPlainDateTime, - &total)) { + ComputeRemainder::Yes, &rounded)) { return false; } } else { + MOZ_ASSERT(IsValidDuration(roundInput)); + if (!::RoundDuration(cx, roundInput, Increment{1}, unit, - TemporalRoundingMode::Trunc, &total)) { + TemporalRoundingMode::Trunc, ComputeRemainder::Yes, + &rounded)) { return false; } } - // Step 23. - args.rval().setNumber(total); + // Step 21. + args.rval().setNumber(rounded.total); return true; } @@ -6605,64 +5602,37 @@ static bool Duration_toString(JSContext* cx, const CallArgs& args) { if (precision.unit != TemporalUnit::Nanosecond || precision.increment != Increment{1}) { // Step 10.a. - auto largestUnit = DefaultTemporalLargestUnit(duration); + auto timeDuration = NormalizeTimeDuration(duration); - // Steps 10.b-c. - auto toRound = Duration{ - 0, - 0, - 0, - 0, - 0, - 0, - duration.seconds, - duration.milliseconds, - duration.microseconds, - duration.nanoseconds, - }; - Duration roundResult; - if (!temporal::RoundDuration(cx, toRound, precision.increment, - precision.unit, roundingMode, &roundResult)) { - return false; - } + // Step 10.b. + auto largestUnit = DefaultTemporalLargestUnit(duration); - // Step 10.d. - auto toBalance = Duration{ - 0, - 0, - 0, - duration.days, - duration.hours, - duration.minutes, - roundResult.seconds, - roundResult.milliseconds, - roundResult.microseconds, - roundResult.nanoseconds, - }; - TimeDuration balanceResult; - if (!BalanceTimeDuration(cx, toBalance, largestUnit, &balanceResult)) { + // Steps 10.c-d. + NormalizedTimeDuration rounded; + if (!RoundDuration(cx, timeDuration, precision.increment, precision.unit, + roundingMode, &rounded)) { return false; } // Step 10.e. + auto balanced = BalanceTimeDuration( + rounded, std::min(largestUnit, TemporalUnit::Second)); + + // Step 10.f. result = { - duration.years, - duration.months, - duration.weeks, - balanceResult.days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, + duration.years, duration.months, + duration.weeks, duration.days + double(balanced.days), + double(balanced.hours), double(balanced.minutes), + double(balanced.seconds), double(balanced.milliseconds), + balanced.microseconds, balanced.nanoseconds, }; + MOZ_ASSERT(IsValidDuration(duration)); } else { // Step 11. result = duration; } - // Step 12. + // Steps 12-13. JSString* str = TemporalDurationToString(cx, result, precision.precision); if (!str) { return false; @@ -6682,14 +5652,13 @@ static bool Duration_toString(JSContext* cx, unsigned argc, Value* vp) { } /** - * Temporal.Duration.prototype.toJSON ( ) + * Temporal.Duration.prototype.toJSON ( ) */ static bool Duration_toJSON(JSContext* cx, const CallArgs& args) { - auto* duration = &args.thisv().toObject().as(); + auto duration = ToDuration(&args.thisv().toObject().as()); - // Step 3. - JSString* str = - TemporalDurationToString(cx, ToDuration(duration), Precision::Auto()); + // Steps 3-4. + JSString* str = TemporalDurationToString(cx, duration, Precision::Auto()); if (!str) { return false; } @@ -6699,7 +5668,7 @@ static bool Duration_toJSON(JSContext* cx, const CallArgs& args) { } /** - * Temporal.Duration.prototype.toJSON ( ) + * Temporal.Duration.prototype.toJSON ( ) */ static bool Duration_toJSON(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. @@ -6711,11 +5680,10 @@ static bool Duration_toJSON(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.toLocaleString ( [ locales [ , options ] ] ) */ static bool Duration_toLocaleString(JSContext* cx, const CallArgs& args) { - auto* duration = &args.thisv().toObject().as(); + auto duration = ToDuration(&args.thisv().toObject().as()); - // Step 3. - JSString* str = - TemporalDurationToString(cx, ToDuration(duration), Precision::Auto()); + // Steps 3-4. + JSString* str = TemporalDurationToString(cx, duration, Precision::Auto()); if (!str) { return false; } diff --git a/js/src/builtin/temporal/Duration.h b/js/src/builtin/temporal/Duration.h index 47708458f4..e1aea4d1d4 100644 --- a/js/src/builtin/temporal/Duration.h +++ b/js/src/builtin/temporal/Duration.h @@ -88,18 +88,167 @@ enum class TemporalUnit; */ int32_t DurationSign(const Duration& duration); +/** + * DurationSign ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +int32_t DurationSign(const DateDuration& duration); + /** * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) */ bool IsValidDuration(const Duration& duration); +#ifdef DEBUG +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool IsValidDuration(const DateDuration& duration); + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool IsValidDuration(const NormalizedDuration& duration); +#endif + /** * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) */ bool ThrowIfInvalidDuration(JSContext* cx, const Duration& duration); +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool ThrowIfInvalidDuration(JSContext* cx, const DateDuration& duration); + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +inline bool IsValidNormalizedTimeDuration( + const NormalizedTimeDuration& duration) { + MOZ_ASSERT(0 <= duration.nanoseconds && duration.nanoseconds <= 999'999'999); + + // Step 4. + // + // The absolute value of the seconds part of normalized time duration must be + // less-or-equal to `2**53 - 1` and the nanoseconds part must be less or equal + // to `999'999'999`. + return NormalizedTimeDuration::min() <= duration && + duration <= NormalizedTimeDuration::max(); +} + +/** + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) + */ +NormalizedTimeDuration NormalizeTimeDuration(int32_t hours, int32_t minutes, + int32_t seconds, + int32_t milliseconds, + int32_t microseconds, + int32_t nanoseconds); + +/** + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) + */ +NormalizedTimeDuration NormalizeTimeDuration(const Duration& duration); + +/** + * CompareNormalizedTimeDuration ( one, two ) + */ +inline int32_t CompareNormalizedTimeDuration( + const NormalizedTimeDuration& one, const NormalizedTimeDuration& two) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(one)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(two)); + + // Step 1. + if (one > two) { + return 1; + } + + // Step 2. + if (one < two) { + return -1; + } + + // Step 3. + return 0; +} + +/** + * NormalizedTimeDurationSign ( d ) + */ +inline int32_t NormalizedTimeDurationSign(const NormalizedTimeDuration& d) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(d)); + + // Steps 1-3. + return CompareNormalizedTimeDuration(d, NormalizedTimeDuration{}); +} + +/** + * Add24HourDaysToNormalizedTimeDuration ( d, days ) + */ +bool Add24HourDaysToNormalizedTimeDuration(JSContext* cx, + const NormalizedTimeDuration& d, + int64_t days, + NormalizedTimeDuration* result); + +/** + * CreateNormalizedDurationRecord ( years, months, weeks, days, norm ) + */ +inline NormalizedDuration CreateNormalizedDurationRecord( + const DateDuration& date, const NormalizedTimeDuration& time) { + MOZ_ASSERT(IsValidDuration(date)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(time)); +#ifdef DEBUG + int64_t dateValues = date.years | date.months | date.weeks | date.days; + int32_t dateSign = dateValues ? dateValues < 0 ? -1 : 1 : 0; + int32_t timeSign = NormalizedTimeDurationSign(time); + MOZ_ASSERT((dateSign * timeSign) >= 0); +#endif + + return {date, time}; +} + +/** + * CreateNormalizedDurationRecord ( years, months, weeks, days, norm ) + */ +inline NormalizedDuration CreateNormalizedDurationRecord( + const Duration& duration) { + return CreateNormalizedDurationRecord(duration.toDateDuration(), + NormalizeTimeDuration(duration)); +} + +/** + * CombineDateAndNormalizedTimeDuration ( dateDurationRecord, norm ) + */ +bool CombineDateAndNormalizedTimeDuration(JSContext* cx, + const DateDuration& date, + const NormalizedTimeDuration& time, + NormalizedDuration* result); + +/** + * CreateNormalizedDurationRecord ( years, months, weeks, days, norm ) + */ +inline bool CreateNormalizedDurationRecord(JSContext* cx, + const DateDuration& date, + const NormalizedTimeDuration& time, + NormalizedDuration* result) { + return CombineDateAndNormalizedTimeDuration(cx, date, time, result); +} + +/** + * NormalizedTimeDurationFromEpochNanosecondsDifference ( one, two ) + */ +NormalizedTimeDuration NormalizedTimeDurationFromEpochNanosecondsDifference( + const Instant& one, const Instant& two); + /** * CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds [ , newTarget ] ) @@ -126,17 +275,15 @@ bool ToTemporalDurationRecord(JSContext* cx, Duration* result); /** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) + * BalanceTimeDuration ( norm, largestUnit ) */ -bool BalanceTimeDuration(JSContext* cx, const Duration& duration, - TemporalUnit largestUnit, TimeDuration* result); +TimeDuration BalanceTimeDuration(const NormalizedTimeDuration& duration, + TemporalUnit largestUnit); /** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) + * BalanceTimeDuration ( norm, largestUnit ) */ -bool BalanceTimeDuration(JSContext* cx, const InstantSpan& nanoseconds, +bool BalanceTimeDuration(JSContext* cx, const NormalizedTimeDuration& duration, TemporalUnit largestUnit, TimeDuration* result); /** @@ -144,61 +291,71 @@ bool BalanceTimeDuration(JSContext* cx, const InstantSpan& nanoseconds, * smallestUnit, plainRelativeTo, calendarRec ) */ bool BalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, TemporalUnit smallestUnit, JS::Handle> plainRelativeTo, JS::Handle calendar, DateDuration* result); /** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * AdjustRoundedDurationDays ( years, months, weeks, days, norm, increment, + * unit, roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, * precalculatedPlainDateTime ) */ -bool AdjustRoundedDurationDays(JSContext* cx, const Duration& duration, +bool AdjustRoundedDurationDays(JSContext* cx, + const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, JS::Handle relativeTo, JS::Handle calendar, JS::Handle timeZone, const PlainDateTime& precalculatedPlainDateTime, - Duration* result); + NormalizedDuration* result); /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -bool RoundDuration(JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Duration* result); +NormalizedTimeDuration RoundDuration(const NormalizedTimeDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode); /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -bool RoundDuration(JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, +bool RoundDuration(JSContext* cx, const NormalizedTimeDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + NormalizedTimeDuration* result); + +/** + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) + */ +bool RoundDuration(JSContext* cx, const NormalizedDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, JS::Handle> plainRelativeTo, - JS::Handle calendar, Duration* result); + JS::Handle calendar, + NormalizedDuration* result); /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -bool RoundDuration(JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, +bool RoundDuration(JSContext* cx, const NormalizedDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, JS::Handle plainRelativeTo, JS::Handle calendar, JS::Handle zonedRelativeTo, JS::Handle timeZone, const PlainDateTime& precalculatedPlainDateTime, - Duration* result); + NormalizedDuration* result); /** * DaysUntil ( earlier, later ) diff --git a/js/src/builtin/temporal/Instant.cpp b/js/src/builtin/temporal/Instant.cpp index 78fc15f313..8e38f3ad51 100644 --- a/js/src/builtin/temporal/Instant.cpp +++ b/js/src/builtin/temporal/Instant.cpp @@ -7,6 +7,7 @@ #include "builtin/temporal/Instant.h" #include "mozilla/Assertions.h" +#include "mozilla/Casting.h" #include "mozilla/CheckedInt.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" @@ -164,43 +165,18 @@ bool js::temporal::IsValidEpochInstant(const Instant& instant) { return Instant::min() <= instant && instant <= Instant::max(); } -static constexpr auto NanosecondsMaxInstantSpan() { - static_assert(BigInt::DigitBits == 64 || BigInt::DigitBits == 32); - - // ±8.64 × 10^21 is the nanoseconds from epoch limit. - // 2 × 8.64 × 10^21 is 172_80000_00000_00000_00000 or 0x3a8_c02c5ea2_de000000. - // Return the BigInt digits of that number for fast BigInt comparisons. - if constexpr (BigInt::DigitBits == 64) { - return std::array{ - BigInt::Digit(0x3a8), - BigInt::Digit(0xc02c'5ea2'de00'0000), - }; - } else { - return std::array{ - BigInt::Digit(0x3a8), - BigInt::Digit(0xc02c'5ea2), - BigInt::Digit(0xde00'0000), - }; - } -} - +#ifdef DEBUG /** * Validates a nanoseconds amount is at most as large as the difference * between two valid nanoseconds from the epoch instants. - * - * Useful when we want to ensure a BigInt doesn't exceed a certain limit. */ -bool js::temporal::IsValidInstantSpan(const BigInt* nanoseconds) { - static constexpr auto spanLimit = NanosecondsMaxInstantSpan(); - return AbsoluteValueIsLessOrEqual(nanoseconds); -} - bool js::temporal::IsValidInstantSpan(const InstantSpan& span) { MOZ_ASSERT(0 <= span.nanoseconds && span.nanoseconds <= 999'999'999); // Steps 1-3. return InstantSpan::min() <= span && span <= InstantSpan::max(); } +#endif /** * Return the BigInt as a 96-bit integer. The BigInt digits must not consist of @@ -258,14 +234,6 @@ Instant js::temporal::ToInstant(const BigInt* epochNanoseconds) { return {seconds, nanos}; } -InstantSpan js::temporal::ToInstantSpan(const BigInt* nanoseconds) { - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); - - auto [seconds, nanos] = - ToInt96(nanoseconds) / ToNanoseconds(TemporalUnit::Second); - return {seconds, nanos}; -} - static BigInt* CreateBigInt(JSContext* cx, const std::array& digits, bool negative) { @@ -300,9 +268,7 @@ static BigInt* CreateBigInt(JSContext* cx, } } -static BigInt* ToEpochBigInt(JSContext* cx, const InstantSpan& instant) { - MOZ_ASSERT(IsValidInstantSpan(instant)); - +static auto ToBigIntDigits(uint64_t seconds, uint32_t nanoseconds) { // Multiplies two uint32_t values and returns the lower 32-bits. The higher // 32-bits are stored in |high|. auto digitMul = [](uint32_t a, uint32_t b, uint32_t* high) { @@ -321,20 +287,6 @@ static BigInt* ToEpochBigInt(JSContext* cx, const InstantSpan& instant) { constexpr uint32_t secToNanos = ToNanoseconds(TemporalUnit::Second); - uint64_t seconds = std::abs(instant.seconds); - uint32_t nanoseconds = instant.nanoseconds; - - // Negative nanoseconds are represented as the difference to 1'000'000'000. - // Convert these back to their absolute value and adjust the seconds part - // accordingly. - // - // For example the nanoseconds from the epoch value |-1n| is represented as - // the instant {seconds: -1, nanoseconds: 999'999'999}. - if (instant.seconds < 0 && nanoseconds != 0) { - nanoseconds = secToNanos - nanoseconds; - seconds -= 1; - } - // uint32_t digits stored in the same order as BigInt digits, i.e. the least // significant digit is stored at index zero. std::array multiplicand = {uint32_t(seconds), @@ -362,93 +314,34 @@ static BigInt* ToEpochBigInt(JSContext* cx, const InstantSpan& instant) { MOZ_ASSERT(newCarry == 0); } - return CreateBigInt(cx, accumulator, instant.seconds < 0); -} - -BigInt* js::temporal::ToEpochNanoseconds(JSContext* cx, - const Instant& instant) { - MOZ_ASSERT(IsValidEpochInstant(instant)); - return ::ToEpochBigInt(cx, InstantSpan{instant.seconds, instant.nanoseconds}); -} - -BigInt* js::temporal::ToEpochNanoseconds(JSContext* cx, - const InstantSpan& instant) { - MOZ_ASSERT(IsValidInstantSpan(instant)); - return ::ToEpochBigInt(cx, instant); -} - -/** - * Return an Instant for the input nanoseconds if the input is less-or-equal to - * the maximum instant span. Otherwise returns nothing. - */ -static mozilla::Maybe NanosecondsToInstantSpan( - double nanoseconds) { - MOZ_ASSERT(IsInteger(nanoseconds)); - - if (auto int96 = Int96::fromInteger(nanoseconds)) { - constexpr auto maximum = Int96{InstantSpan::max().toSeconds()} * - ToNanoseconds(TemporalUnit::Second); - - // Accept if the value is less-or-equal to the maximum instant span. - if (int96->abs() <= maximum) { - // Split into seconds and nanoseconds. - auto [seconds, nanos] = *int96 / ToNanoseconds(TemporalUnit::Second); - - auto result = InstantSpan{seconds, nanos}; - MOZ_ASSERT(IsValidInstantSpan(result)); - return mozilla::Some(result); - } - } - return mozilla::Nothing(); + return accumulator; } -/** - * Return an Instant for the input microseconds if the input is less-or-equal to - * the maximum instant span. Otherwise returns nothing. - */ -static mozilla::Maybe MicrosecondsToInstantSpan( - double microseconds) { - MOZ_ASSERT(IsInteger(microseconds)); +template +static BigInt* ToBigInt(JSContext* cx, + const SecondsAndNanoseconds& secondsAndNanoseconds) { + uint64_t seconds = std::abs(secondsAndNanoseconds.seconds); + uint32_t nanoseconds = secondsAndNanoseconds.nanoseconds; - constexpr int64_t spanLimit = InstantSpan::max().toSeconds(); - constexpr int64_t secToMicros = ToNanoseconds(TemporalUnit::Second) / - ToNanoseconds(TemporalUnit::Microsecond); - constexpr int32_t microToNanos = ToNanoseconds(TemporalUnit::Microsecond); - - // Fast path for the common case. - if (microseconds == 0) { - return mozilla::Some(InstantSpan{}); - } - - // Reject if the value is larger than the maximum instant span. - if (std::abs(microseconds) > double(spanLimit) * double(secToMicros)) { - return mozilla::Nothing(); + // Negative nanoseconds are represented as the difference to 1'000'000'000. + // Convert these back to their absolute value and adjust the seconds part + // accordingly. + // + // For example the nanoseconds from the epoch value |-1n| is represented as + // the instant {seconds: -1, nanoseconds: 999'999'999}. + if (secondsAndNanoseconds.seconds < 0 && nanoseconds != 0) { + nanoseconds = ToNanoseconds(TemporalUnit::Second) - nanoseconds; + seconds -= 1; } - // |spanLimit| in microseconds is below UINT64_MAX, so we can use uint64 in - // the following computations. - static_assert(double(spanLimit) * double(secToMicros) <= double(UINT64_MAX)); - - // Use the absolute value and convert it then into uint64_t. - uint64_t absMicros = uint64_t(std::abs(microseconds)); - - // Seconds and remainder are small enough to fit into int64_t resp. int32_t. - int64_t seconds = absMicros / uint64_t(secToMicros); - int32_t remainder = absMicros % uint64_t(secToMicros); - - // Correct the sign of |seconds| and |remainder|, and then constrain - // |remainder| to the range [0, 999'999]. - if (microseconds < 0) { - seconds *= -1; - if (remainder != 0) { - seconds -= 1; - remainder = secToMicros - remainder; - } - } + auto digits = ToBigIntDigits(seconds, nanoseconds); + return CreateBigInt(cx, digits, secondsAndNanoseconds.seconds < 0); +} - InstantSpan result = {seconds, remainder * microToNanos}; - MOZ_ASSERT(IsValidInstantSpan(result)); - return mozilla::Some(result); +BigInt* js::temporal::ToEpochNanoseconds(JSContext* cx, + const Instant& instant) { + MOZ_ASSERT(IsValidEpochInstant(instant)); + return ::ToBigInt(cx, instant); } /** @@ -456,7 +349,7 @@ static mozilla::Maybe MicrosecondsToInstantSpan( * microsecond, nanosecond [ , offsetNanoseconds ] ) */ Instant js::temporal::GetUTCEpochNanoseconds(const PlainDateTime& dateTime) { - auto& [date, time] = dateTime; + const auto& [date, time] = dateTime; // Step 1. MOZ_ASSERT(IsValidISODateTime(dateTime)); @@ -579,13 +472,13 @@ Wrapped js::temporal::ToTemporalInstant(JSContext* cx, } } - // Steps 1.b-d and 3-6 + // Steps 1.b-d and 3-7 Instant epochNanoseconds; if (!ToTemporalInstant(cx, item, &epochNanoseconds)) { return nullptr; } - // Step 7. + // Step 8. return CreateTemporalInstant(cx, epochNanoseconds); } @@ -635,25 +528,25 @@ bool js::temporal::ToTemporalInstant(JSContext* cx, Handle item, } MOZ_ASSERT(std::abs(offset) < ToNanoseconds(TemporalUnit::Day)); - // Step 6. (Reordered) + // Steps 5-6. (Reordered) if (!ISODateTimeWithinLimits(dateTime)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INSTANT_INVALID); return false; } - // Step 5. + // Step 4. auto epochNanoseconds = GetUTCEpochNanoseconds(dateTime, InstantSpan::fromNanoseconds(offset)); - // Step 6. + // Step 7. if (!IsValidEpochInstant(epochNanoseconds)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INSTANT_INVALID); return false; } - // Step 7. + // Step 8. *result = epochNanoseconds; return true; } @@ -663,181 +556,75 @@ bool js::temporal::ToTemporalInstant(JSContext* cx, Handle item, * microseconds, nanoseconds ) */ bool js::temporal::AddInstant(JSContext* cx, const Instant& instant, - const Duration& duration, Instant* result) { + const NormalizedTimeDuration& duration, + Instant* result) { MOZ_ASSERT(IsValidEpochInstant(instant)); - MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(duration.years == 0); - MOZ_ASSERT(duration.months == 0); - MOZ_ASSERT(duration.weeks == 0); - MOZ_ASSERT(duration.days == 0); - - do { - auto nanoseconds = NanosecondsToInstantSpan(duration.nanoseconds); - if (!nanoseconds) { - break; - } - MOZ_ASSERT(IsValidInstantSpan(*nanoseconds)); - - auto microseconds = MicrosecondsToInstantSpan(duration.microseconds); - if (!microseconds) { - break; - } - MOZ_ASSERT(IsValidInstantSpan(*microseconds)); - - // Overflows for millis/seconds/minutes/hours always result in an invalid - // instant. - - int64_t milliseconds; - if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { - break; - } + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); - int64_t seconds; - if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { - break; - } + // Step 1. (Inlined AddNormalizedTimeDurationToEpochNanoseconds) + auto r = instant + duration.to(); - int64_t minutes; - if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { - break; - } - - int64_t hours; - if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { - break; - } - - // Compute the overall amount of milliseconds to add. - mozilla::CheckedInt64 millis = hours; - millis *= 60; - millis += minutes; - millis *= 60; - millis += seconds; - millis *= 1000; - millis += milliseconds; - if (!millis.isValid()) { - break; - } - - auto milli = InstantSpan::fromMilliseconds(millis.value()); - if (!IsValidInstantSpan(milli)) { - break; - } - - // Compute the overall instant span. - auto span = milli + *microseconds + *nanoseconds; - if (!IsValidInstantSpan(span)) { - break; - } - - *result = instant + span; - if (IsValidEpochInstant(*result)) { - return true; - } - } while (false); + // Step 2. + if (!IsValidEpochInstant(r)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_INSTANT_INVALID); - return false; + // Step 3. + *result = r; + return true; } /** - * DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, - * roundingMode ) + * DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, roundingMode ) */ -bool js::temporal::DifferenceInstant(JSContext* cx, const Instant& ns1, - const Instant& ns2, - Increment roundingIncrement, - TemporalUnit smallestUnit, - TemporalUnit largestUnit, - TemporalRoundingMode roundingMode, - Duration* result) { +NormalizedTimeDuration js::temporal::DifferenceInstant( + const Instant& ns1, const Instant& ns2, Increment roundingIncrement, + TemporalUnit smallestUnit, TemporalRoundingMode roundingMode) { MOZ_ASSERT(IsValidEpochInstant(ns1)); MOZ_ASSERT(IsValidEpochInstant(ns2)); - MOZ_ASSERT(largestUnit > TemporalUnit::Day); - MOZ_ASSERT(largestUnit <= smallestUnit); + MOZ_ASSERT(smallestUnit > TemporalUnit::Day); MOZ_ASSERT(roundingIncrement <= MaximumTemporalDurationRoundingIncrement(smallestUnit)); // Step 1. - auto diff = ns2 - ns1; - MOZ_ASSERT(IsValidInstantSpan(diff)); - - // Negative nanoseconds are represented as the difference to 1'000'000'000. - auto [seconds, nanoseconds] = diff; - if (seconds < 0 && nanoseconds != 0) { - seconds += 1; - nanoseconds -= ToNanoseconds(TemporalUnit::Second); - } - - // Steps 2-5. - Duration duration = { - 0, - 0, - 0, - 0, - 0, - 0, - double(seconds), - double((nanoseconds / 1000'000) % 1000), - double((nanoseconds / 1000) % 1000), - double(nanoseconds % 1000), - }; - MOZ_ASSERT(IsValidDuration(duration)); + auto diff = NormalizedTimeDurationFromEpochNanosecondsDifference(ns2, ns1); + MOZ_ASSERT(IsValidInstantSpan(diff.to())); - // Step 6. + // Step 2. if (smallestUnit == TemporalUnit::Nanosecond && roundingIncrement == Increment{1}) { - TimeDuration balanced; - if (!BalanceTimeDuration(cx, duration, largestUnit, &balanced)) { - return false; - } - MOZ_ASSERT(balanced.days == 0); - - *result = balanced.toDuration().time(); - return true; - } - - // Steps 7-8. - Duration roundResult; - if (!temporal::RoundDuration(cx, duration, roundingIncrement, smallestUnit, - roundingMode, &roundResult)) { - return false; - } - - // Step 9. - MOZ_ASSERT(roundResult.days == 0); - - // Step 10. - TimeDuration balanced; - if (!BalanceTimeDuration(cx, roundResult, largestUnit, &balanced)) { - return false; + return diff; } - MOZ_ASSERT(balanced.days == 0); - *result = balanced.toDuration().time(); - return true; + // Steps 3-4. + return RoundDuration(diff, roundingIncrement, smallestUnit, roundingMode); } /** * RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode ) */ -static bool RoundNumberToIncrementAsIfPositive( - JSContext* cx, const Instant& x, int64_t increment, - TemporalRoundingMode roundingMode, Instant* result) { +static Instant RoundNumberToIncrementAsIfPositive( + const Instant& x, int64_t increment, TemporalRoundingMode roundingMode) { + MOZ_ASSERT(IsValidEpochInstant(x)); + MOZ_ASSERT(increment > 0); + MOZ_ASSERT(increment <= ToNanoseconds(TemporalUnit::Day)); + // This operation is equivalent to adjusting the rounding mode through // |ToPositiveRoundingMode| and then calling |RoundNumberToIncrement|. - return RoundNumberToIncrement(cx, x, increment, - ToPositiveRoundingMode(roundingMode), result); + auto rounded = RoundNumberToIncrement(x.toNanoseconds(), Int128{increment}, + ToPositiveRoundingMode(roundingMode)); + return Instant::fromNanoseconds(rounded); } /** * RoundTemporalInstant ( ns, increment, unit, roundingMode ) */ -bool js::temporal::RoundTemporalInstant(JSContext* cx, const Instant& ns, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, - Instant* result) { +Instant js::temporal::RoundTemporalInstant(const Instant& ns, + Increment increment, + TemporalUnit unit, + TemporalRoundingMode roundingMode) { MOZ_ASSERT(IsValidEpochInstant(ns)); MOZ_ASSERT(increment >= Increment::min()); MOZ_ASSERT(uint64_t(increment.value()) <= ToNanoseconds(TemporalUnit::Day)); @@ -851,7 +638,7 @@ bool js::temporal::RoundTemporalInstant(JSContext* cx, const Instant& ns, // Step 7. return RoundNumberToIncrementAsIfPositive( - cx, ns, increment.value() * toNanoseconds, roundingMode, result); + ns, increment.value() * toNanoseconds, roundingMode); } /** @@ -903,19 +690,23 @@ static bool DifferenceTemporalInstant(JSContext* cx, } // Step 5. - Duration difference; - if (!DifferenceInstant(cx, instant, other, settings.roundingIncrement, - settings.smallestUnit, settings.largestUnit, - settings.roundingMode, &difference)) { + auto difference = + DifferenceInstant(instant, other, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode); + + // Step 6. + TimeDuration balanced; + if (!BalanceTimeDuration(cx, difference, settings.largestUnit, &balanced)) { return false; } - // Step 6. + // Step 7. + auto duration = balanced.toDuration(); if (operation == TemporalDifference::Since) { - difference = difference.negate(); + duration = duration.negate(); } - auto* obj = CreateTemporalDuration(cx, difference); + auto* obj = CreateTemporalDuration(cx, duration); if (!obj) { return false; } @@ -959,13 +750,15 @@ static bool AddDurationToOrSubtractDurationFromInstant( if (operation == InstantDuration::Subtract) { duration = duration.negate(); } + auto timeDuration = NormalizeTimeDuration(duration); + // Step 8. Instant ns; - if (!AddInstant(cx, epochNanoseconds, duration, &ns)) { + if (!AddInstant(cx, epochNanoseconds, timeDuration, &ns)) { return false; } - // Step 8. + // Step 9. auto* result = CreateTemporalInstant(cx, ns); if (!result) { return false; @@ -1063,7 +856,8 @@ static bool Instant_fromEpochSeconds(JSContext* cx, unsigned argc, Value* vp) { } // Step 5. - auto* result = CreateTemporalInstant(cx, Instant::fromSeconds(epochSeconds)); + int64_t seconds = mozilla::AssertedCast(epochSeconds); + auto* result = CreateTemporalInstant(cx, Instant::fromSeconds(seconds)); if (!result) { return false; } @@ -1106,8 +900,9 @@ static bool Instant_fromEpochMilliseconds(JSContext* cx, unsigned argc, } // Step 5. + int64_t milliseconds = mozilla::AssertedCast(epochMilliseconds); auto* result = - CreateTemporalInstant(cx, Instant::fromMilliseconds(epochMilliseconds)); + CreateTemporalInstant(cx, Instant::fromMilliseconds(milliseconds)); if (!result) { return false; } @@ -1413,7 +1208,7 @@ static bool Instant_round(JSContext* cx, const CallArgs& args) { } // Steps 10-15. - uint64_t maximum = UnitsPerDay(smallestUnit); + int64_t maximum = UnitsPerDay(smallestUnit); // Step 16. if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, @@ -1423,11 +1218,8 @@ static bool Instant_round(JSContext* cx, const CallArgs& args) { } // Step 17. - Instant roundedNs; - if (!RoundTemporalInstant(cx, instant, roundingIncrement, smallestUnit, - roundingMode, &roundedNs)) { - return false; - } + auto roundedNs = RoundTemporalInstant(instant, roundingIncrement, + smallestUnit, roundingMode); // Step 18. auto* result = CreateTemporalInstant(cx, roundedNs); @@ -1535,11 +1327,8 @@ static bool Instant_toString(JSContext* cx, const CallArgs& args) { } // Step 12. - Instant ns; - if (!RoundTemporalInstant(cx, instant, precision.increment, precision.unit, - roundingMode, &ns)) { - return false; - } + auto ns = RoundTemporalInstant(instant, precision.increment, precision.unit, + roundingMode); // Step 13. Rooted roundedInstant(cx, CreateTemporalInstant(cx, ns)); diff --git a/js/src/builtin/temporal/Instant.h b/js/src/builtin/temporal/Instant.h index edce677d10..9ad7f159db 100644 --- a/js/src/builtin/temporal/Instant.h +++ b/js/src/builtin/temporal/Instant.h @@ -69,15 +69,12 @@ bool IsValidEpochNanoseconds(const JS::BigInt* epochNanoseconds); */ bool IsValidEpochInstant(const Instant& instant); +#ifdef DEBUG /** * Return true if the input is within the valid instant span limits. */ bool IsValidInstantSpan(const InstantSpan& span); - -/** - * Return true if the input is within the valid instant span limits. - */ -bool IsValidInstantSpan(const JS::BigInt* nanoseconds); +#endif /** * Convert a BigInt to an instant. The input must be a valid epoch nanoseconds @@ -85,22 +82,11 @@ bool IsValidInstantSpan(const JS::BigInt* nanoseconds); */ Instant ToInstant(const JS::BigInt* epochNanoseconds); -/** - * Convert a BigInt to an instant span. The input must be a valid epoch - * nanoseconds span value. - */ -InstantSpan ToInstantSpan(const JS::BigInt* nanoseconds); - /** * Convert an instant to a BigInt. The input must be a valid epoch instant. */ JS::BigInt* ToEpochNanoseconds(JSContext* cx, const Instant& instant); -/** - * Convert an instant span to a BigInt. The input must be a valid instant span. - */ -JS::BigInt* ToEpochNanoseconds(JSContext* cx, const InstantSpan& instant); - /** * ToTemporalInstant ( item ) */ @@ -134,25 +120,23 @@ Instant GetUTCEpochNanoseconds(const PlainDateTime& dateTime, /** * RoundTemporalInstant ( ns, increment, unit, roundingMode ) */ -bool RoundTemporalInstant(JSContext* cx, const Instant& ns, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Instant* result); +Instant RoundTemporalInstant(const Instant& ns, Increment increment, + TemporalUnit unit, + TemporalRoundingMode roundingMode); /** - * AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) + * AddInstant ( epochNanoseconds, norm ) */ -bool AddInstant(JSContext* cx, const Instant& instant, const Duration& duration, - Instant* result); +bool AddInstant(JSContext* cx, const Instant& instant, + const NormalizedTimeDuration& duration, Instant* result); /** - * DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, - * roundingMode ) + * DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, roundingMode ) */ -bool DifferenceInstant(JSContext* cx, const Instant& ns1, const Instant& ns2, - Increment roundingIncrement, TemporalUnit smallestUnit, - TemporalUnit largestUnit, - TemporalRoundingMode roundingMode, Duration* result); +NormalizedTimeDuration DifferenceInstant(const Instant& ns1, const Instant& ns2, + Increment roundingIncrement, + TemporalUnit smallestUnit, + TemporalRoundingMode roundingMode); } /* namespace js::temporal */ diff --git a/js/src/builtin/temporal/Int128.cpp b/js/src/builtin/temporal/Int128.cpp new file mode 100644 index 0000000000..e712a64be6 --- /dev/null +++ b/js/src/builtin/temporal/Int128.cpp @@ -0,0 +1,161 @@ +/* -*- 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/Int128.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/MathAlgorithms.h" + +#include + +using namespace js; +using namespace js::temporal; + +double Uint128::toDouble(const Uint128& x, bool negative) { + // Simplified version of |BigInt::numberValue()| for DigitBits=64. See the + // comments in |BigInt::numberValue()| for how this code works. + + using Double = mozilla::FloatingPoint; + constexpr uint8_t ExponentShift = Double::kExponentShift; + constexpr uint8_t SignificandWidth = Double::kSignificandWidth; + constexpr unsigned ExponentBias = Double::kExponentBias; + constexpr uint8_t SignShift = Double::kExponentWidth + SignificandWidth; + + constexpr uint64_t MaxIntegralPrecisionDouble = uint64_t(1) + << (SignificandWidth + 1); + + // We compute the final mantissa of the result, shifted upward to the top of + // the `uint64_t` space -- plus an extra bit to detect potential rounding. + constexpr uint8_t BitsNeededForShiftedMantissa = SignificandWidth + 1; + + uint64_t shiftedMantissa = 0; + uint64_t exponent = 0; + + // If the extra bit is set, correctly rounding the result may require + // examining all lower-order bits. Also compute 1) the index of the Digit + // storing the extra bit, and 2) whether bits beneath the extra bit in that + // Digit are nonzero so we can round if needed. + uint64_t bitsBeneathExtraBitInDigitContainingExtraBit = 0; + + if (x.high == 0) { + uint64_t msd = x.low; + + // Fast path for the likely-common case of up to a uint64_t of magnitude not + // exceeding integral precision in IEEE-754. + if (msd <= MaxIntegralPrecisionDouble) { + return negative ? -double(msd) : +double(msd); + } + + const uint8_t msdLeadingZeroes = mozilla::CountLeadingZeroes64(msd); + MOZ_ASSERT(0 <= msdLeadingZeroes && msdLeadingZeroes <= 10, + "leading zeroes is at most 10 when the fast path isn't taken"); + + exponent = 64 - msdLeadingZeroes - 1; + + // Omit the most significant bit: the IEEE-754 format includes this bit + // implicitly for all double-precision integers. + const uint8_t msdIgnoredBits = msdLeadingZeroes + 1; + MOZ_ASSERT(1 <= msdIgnoredBits && msdIgnoredBits <= 11); + + const uint8_t msdIncludedBits = 64 - msdIgnoredBits; + MOZ_ASSERT(53 <= msdIncludedBits && msdIncludedBits <= 63); + MOZ_ASSERT(msdIncludedBits >= BitsNeededForShiftedMantissa); + + // Munge the most significant bits of the number into proper + // position in an IEEE-754 double and go to town. + + // Shift `msd`'s contributed bits upward to remove high-order zeroes and the + // highest set bit (which is implicit in IEEE-754 integral values so must be + // removed) and to add low-order zeroes. (Lower-order garbage bits are + // discarded when `shiftedMantissa` is converted to a real mantissa.) + shiftedMantissa = msd << (64 - msdIncludedBits); + + // Add shifted bits to `shiftedMantissa` until we have a complete mantissa + // and an extra bit. + const uint8_t countOfBitsInDigitBelowExtraBit = + 64 - BitsNeededForShiftedMantissa - msdIgnoredBits; + bitsBeneathExtraBitInDigitContainingExtraBit = + msd & ((uint64_t(1) << countOfBitsInDigitBelowExtraBit) - 1); + } else { + uint64_t msd = x.high; + uint64_t second = x.low; + + uint8_t msdLeadingZeroes = mozilla::CountLeadingZeroes64(msd); + + exponent = 2 * 64 - msdLeadingZeroes - 1; + + // Munge the most significant bits of the number into proper + // position in an IEEE-754 double and go to town. + + // Omit the most significant bit: the IEEE-754 format includes this bit + // implicitly for all double-precision integers. + const uint8_t msdIgnoredBits = msdLeadingZeroes + 1; + const uint8_t msdIncludedBits = 64 - msdIgnoredBits; + + // Shift `msd`'s contributed bits upward to remove high-order zeroes and the + // highest set bit (which is implicit in IEEE-754 integral values so must be + // removed) and to add low-order zeroes. (Lower-order garbage bits are + // discarded when `shiftedMantissa` is converted to a real mantissa.) + shiftedMantissa = msdIncludedBits == 0 ? 0 : msd << (64 - msdIncludedBits); + + // Add shifted bits to `shiftedMantissa` until we have a complete mantissa + // and an extra bit. + if (msdIncludedBits >= BitsNeededForShiftedMantissa) { + const uint8_t countOfBitsInDigitBelowExtraBit = + 64 - BitsNeededForShiftedMantissa - msdIgnoredBits; + bitsBeneathExtraBitInDigitContainingExtraBit = + msd & ((uint64_t(1) << countOfBitsInDigitBelowExtraBit) - 1); + + if (bitsBeneathExtraBitInDigitContainingExtraBit == 0) { + bitsBeneathExtraBitInDigitContainingExtraBit = second; + } + } else { + shiftedMantissa |= second >> msdIncludedBits; + + const uint8_t countOfBitsInSecondDigitBelowExtraBit = + (msdIncludedBits + 64) - BitsNeededForShiftedMantissa; + bitsBeneathExtraBitInDigitContainingExtraBit = + second << (64 - countOfBitsInSecondDigitBelowExtraBit); + } + } + + constexpr uint64_t LeastSignificantBit = uint64_t(1) + << (64 - SignificandWidth); + constexpr uint64_t ExtraBit = LeastSignificantBit >> 1; + + // The extra bit must be set for rounding to change the mantissa. + if ((shiftedMantissa & ExtraBit) != 0) { + bool shouldRoundUp; + if (shiftedMantissa & LeastSignificantBit) { + // If the lowest mantissa bit is set, it doesn't matter what lower bits + // are: nearest-even rounds up regardless. + shouldRoundUp = true; + } else { + // If the lowest mantissa bit is unset, *all* lower bits are relevant. + // All-zero bits below the extra bit situates `x` halfway between two + // values, and the nearest *even* value lies downward. But if any bit + // below the extra bit is set, `x` is closer to the rounded-up value. + shouldRoundUp = bitsBeneathExtraBitInDigitContainingExtraBit != 0; + } + + if (shouldRoundUp) { + // Add one to the significand bits. If they overflow, the exponent must + // also be increased. If *that* overflows, return the correct infinity. + uint64_t before = shiftedMantissa; + shiftedMantissa += ExtraBit; + if (shiftedMantissa < before) { + exponent++; + } + } + } + + uint64_t significandBits = shiftedMantissa >> (64 - SignificandWidth); + uint64_t signBit = uint64_t(negative ? 1 : 0) << SignShift; + uint64_t exponentBits = (exponent + ExponentBias) << ExponentShift; + return mozilla::BitwiseCast(signBit | exponentBits | significandBits); +} diff --git a/js/src/builtin/temporal/Int128.h b/js/src/builtin/temporal/Int128.h new file mode 100644 index 0000000000..b32edb4da5 --- /dev/null +++ b/js/src/builtin/temporal/Int128.h @@ -0,0 +1,742 @@ +/* -*- 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/. */ + +#ifndef builtin_temporal_Int128_h +#define builtin_temporal_Int128_h + +#include "mozilla/Assertions.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/MathAlgorithms.h" + +#include +#include +#include +#include + +namespace js::temporal { + +class Int128; +class Uint128; + +/** + * Unsigned 128-bit integer, implemented as a pair of unsigned 64-bit integers. + * + * Supports all basic arithmetic operators. + */ +class alignas(16) Uint128 final { +#if MOZ_LITTLE_ENDIAN() + uint64_t low = 0; + uint64_t high = 0; +#else + uint64_t high = 0; + uint64_t low = 0; +#endif + + friend class Int128; + + constexpr Uint128(uint64_t low, uint64_t high) : low(low), high(high) {} + + /** + * Return the high double-word of the multiplication of `u * v`. + * + * Based on "Multiply high unsigned" from Hacker's Delight, 2nd edition, + * figure 8-2. + */ + static constexpr uint64_t mulhu(uint64_t u, uint64_t v) { + uint64_t u0 = u & 0xffff'ffff; + uint64_t u1 = u >> 32; + + uint64_t v0 = v & 0xffff'ffff; + uint64_t v1 = v >> 32; + + uint64_t w0 = u0 * v0; + uint64_t t = u1 * v0 + (w0 >> 32); + uint64_t w1 = t & 0xffff'ffff; + uint64_t w2 = t >> 32; + w1 = u0 * v1 + w1; + return u1 * v1 + w2 + (w1 >> 32); + } + + /** + * Based on "Unsigned doubleword division from long division" from + * Hacker's Delight, 2nd edition, figure 9-5. + */ + static constexpr std::pair udivdi(const Uint128& u, + const Uint128& v) { + MOZ_ASSERT(v != Uint128{}); + + // If v < 2**64 + if (v.high == 0) { + // If u < 2**64 + if (u.high == 0) { + // Prefer built-in division if possible. + return {Uint128{u.low / v.low, 0}, Uint128{u.low % v.low, 0}}; + } + + // If u/v cannot overflow, just do one division. + if (Uint128{u.high, 0} < v) { + auto [q, r] = divlu(u.high, u.low, v.low); + return {Uint128{q, 0}, Uint128{r, 0}}; + } + + // If u/v would overflow: Break u up into two halves. + + // First quotient digit and first remainder, < v. + auto [q1, r1] = divlu(0, u.high, v.low); + + // Second quotient digit. + auto [q0, r0] = divlu(r1, u.low, v.low); + + // Return quotient and remainder. + return {Uint128{q0, q1}, Uint128{r0, 0}}; + } + + // Here v >= 2**64. + + // 0 <= n <= 63 + auto n = mozilla::CountLeadingZeroes64(v.high); + + // Normalize the divisor so its MSB is 1. + auto v1 = (v << n).high; + + // To ensure no overflow. + auto u1 = u >> 1; + + // Get quotient from divide unsigned instruction. + auto [q1, r1] = divlu(u1.high, u1.low, v1); + + // Undo normalization and division of u by 2. + auto q0 = (Uint128{q1, 0} << n) >> 63; + + // Make q0 correct or too small by 1. + if (q0 != Uint128{0}) { + q0 -= Uint128{1}; + } + + // Now q0 is correct. + auto r0 = u - q0 * v; + if (r0 >= v) { + q0 += Uint128{1}; + r0 -= v; + } + + // Return quotient and remainder. + return {q0, r0}; + } + + /** + * Based on "Divide long unsigned, using fullword division instructions" from + * Hacker's Delight, 2nd edition, figure 9-3. + */ + static constexpr std::pair divlu(uint64_t u1, uint64_t u0, + uint64_t v) { + // Number base (32 bits). + constexpr uint64_t base = 4294967296; + + // If overflow, set the remainder to an impossible value and return the + // largest possible quotient. + if (u1 >= v) { + return {UINT64_MAX, UINT64_MAX}; + } + + // Shift amount for normalization. (0 <= s <= 63) + int64_t s = mozilla::CountLeadingZeroes64(v); + + // Normalize the divisor. + v = v << s; + + // Normalized divisor digits. + // + // Break divisor up into two 32-bit digits. + uint64_t vn1 = v >> 32; + uint64_t vn0 = uint32_t(v); + + // Dividend digit pairs. + // + // Shift dividend left. + uint64_t un32 = (u1 << s) | ((u0 >> ((64 - s) & 63)) & (-s >> 63)); + uint64_t un10 = u0 << s; + + // Normalized dividend least significant digits. + // + // Break right half of dividend into two digits. + uint64_t un1 = un10 >> 32; + uint64_t un0 = uint32_t(un10); + + // Compute the first quotient digit and its remainder. + uint64_t q1 = un32 / vn1; + uint64_t rhat = un32 - q1 * vn1; + while (q1 >= base || q1 * vn0 > base * rhat + un1) { + q1 -= 1; + rhat += vn1; + if (rhat >= base) { + break; + } + } + + // Multiply and subtract. + uint64_t un21 = un32 * base + un1 - q1 * v; + + // Compute the second quotient digit and its remainder. + uint64_t q0 = un21 / vn1; + rhat = un21 - q0 * vn1; + while (q0 >= base || q0 * vn0 > base * rhat + un0) { + q0 -= 1; + rhat += vn1; + if (rhat >= base) { + break; + } + } + + // Return the quotient and remainder. + uint64_t q = q1 * base + q0; + uint64_t r = (un21 * base + un0 - q0 * v) >> s; + return {q, r}; + } + + static double toDouble(const Uint128& x, bool negative); + + public: + constexpr Uint128() = default; + constexpr Uint128(const Uint128&) = default; + + explicit constexpr Uint128(uint64_t value) + : Uint128(uint64_t(value), uint64_t(0)) {} + + constexpr bool operator==(const Uint128& other) const { + return low == other.low && high == other.high; + } + + constexpr bool operator<(const Uint128& other) const { + if (high == other.high) { + return low < other.low; + } + return high < other.high; + } + + // Other operators are implemented in terms of operator== and operator<. + constexpr bool operator!=(const Uint128& other) const { + return !(*this == other); + } + constexpr bool operator>(const Uint128& other) const { return other < *this; } + constexpr bool operator<=(const Uint128& other) const { + return !(other < *this); + } + constexpr bool operator>=(const Uint128& other) const { + return !(*this < other); + } + + explicit constexpr operator bool() const { return !(*this == Uint128{}); } + + explicit constexpr operator int8_t() const { return int8_t(low); } + explicit constexpr operator int16_t() const { return int16_t(low); } + explicit constexpr operator int32_t() const { return int32_t(low); } + explicit constexpr operator int64_t() const { return int64_t(low); } + + explicit constexpr operator uint8_t() const { return uint8_t(low); } + explicit constexpr operator uint16_t() const { return uint16_t(low); } + explicit constexpr operator uint32_t() const { return uint32_t(low); } + explicit constexpr operator uint64_t() const { return uint64_t(low); } + + explicit constexpr operator Int128() const; + + explicit operator double() const { return toDouble(*this, false); } + + constexpr Uint128 operator+(const Uint128& other) const { + // "§2-16 Double-Length Add/Subtract" from Hacker's Delight, 2nd edition. + Uint128 result; + result.low = low + other.low; + result.high = high + other.high + static_cast(result.low < low); + return result; + } + + constexpr Uint128 operator-(const Uint128& other) const { + // "§2-16 Double-Length Add/Subtract" from Hacker's Delight, 2nd edition. + Uint128 result; + result.low = low - other.low; + result.high = high - other.high - static_cast(low < other.low); + return result; + } + + constexpr Uint128 operator*(const Uint128& other) const { + uint64_t w01 = low * other.high; + uint64_t w10 = high * other.low; + uint64_t w00 = mulhu(low, other.low); + + uint64_t w1 = w00 + w10 + w01; + uint64_t w0 = low * other.low; + + return Uint128{w0, w1}; + } + + /** + * Return the quotient and remainder of the division. + */ + constexpr std::pair divrem(const Uint128& divisor) const { + return udivdi(*this, divisor); + } + + constexpr Uint128 operator/(const Uint128& other) const { + auto [quot, rem] = divrem(other); + return quot; + } + + constexpr Uint128 operator%(const Uint128& other) const { + auto [quot, rem] = divrem(other); + return rem; + } + + constexpr Uint128 operator<<(int shift) const { + MOZ_ASSERT(0 <= shift && shift <= 127, "undefined shift amount"); + + // Ensure the shift amount is in range. + shift &= 127; + + // "§2-17 Double-Length Shifts" from Hacker's Delight, 2nd edition. + if (shift >= 64) { + uint64_t y0 = 0; + uint64_t y1 = low << (shift - 64); + return Uint128{y0, y1}; + } + uint64_t y0 = low << shift; + uint64_t y1 = (high << shift) | (low >> (64 - shift)); + return Uint128{y0, y1}; + } + + constexpr Uint128 operator>>(int shift) const { + MOZ_ASSERT(0 <= shift && shift <= 127, "undefined shift amount"); + + // Ensure the shift amount is in range. + shift &= 127; + + // "§2-17 Double-Length Shifts" from Hacker's Delight, 2nd edition. + if (shift >= 64) { + uint64_t y0 = high >> (shift - 64); + uint64_t y1 = 0; + return Uint128{y0, y1}; + } + uint64_t y0 = low >> shift | high << (64 - shift); + uint64_t y1 = high >> shift; + return Uint128{y0, y1}; + } + + constexpr Uint128 operator&(const Uint128& other) const { + return Uint128{low & other.low, high & other.high}; + } + + constexpr Uint128 operator|(const Uint128& other) const { + return Uint128{low | other.low, high | other.high}; + } + + constexpr Uint128 operator^(const Uint128& other) const { + return Uint128{low ^ other.low, high ^ other.high}; + } + + constexpr Uint128 operator~() const { return Uint128{~low, ~high}; } + + constexpr Uint128 operator-() const { return Uint128{} - *this; } + + constexpr Uint128 operator+() const { return *this; } + + constexpr Uint128& operator++() { + *this = *this + Uint128{1, 0}; + return *this; + } + + constexpr Uint128 operator++(int) { + auto result = *this; + ++*this; + return result; + } + + constexpr Uint128& operator--() { + *this = *this - Uint128{1, 0}; + return *this; + } + + constexpr Uint128 operator--(int) { + auto result = *this; + --*this; + return result; + } + + constexpr Uint128 operator+=(const Uint128& other) { + *this = *this + other; + return *this; + } + + constexpr Uint128 operator-=(const Uint128& other) { + *this = *this - other; + return *this; + } + + constexpr Uint128 operator*=(const Uint128& other) { + *this = *this * other; + return *this; + } + + constexpr Uint128 operator/=(const Uint128& other) { + *this = *this / other; + return *this; + } + + constexpr Uint128 operator%=(const Uint128& other) { + *this = *this % other; + return *this; + } + + constexpr Uint128 operator&=(const Uint128& other) { + *this = *this & other; + return *this; + } + + constexpr Uint128 operator|=(const Uint128& other) { + *this = *this | other; + return *this; + } + + constexpr Uint128 operator^=(const Uint128& other) { + *this = *this ^ other; + return *this; + } + + constexpr Uint128 operator<<=(int shift) { + *this = *this << shift; + return *this; + } + + constexpr Uint128 operator>>=(int shift) { + *this = *this >> shift; + return *this; + } +}; + +/** + * Signed 128-bit integer, implemented as a pair of unsigned 64-bit integers. + * + * Supports all basic arithmetic operators. + */ +class alignas(16) Int128 final { +#if MOZ_LITTLE_ENDIAN() + uint64_t low = 0; + uint64_t high = 0; +#else + uint64_t high = 0; + uint64_t low = 0; +#endif + + friend class Uint128; + + constexpr Int128(uint64_t low, uint64_t high) : low(low), high(high) {} + + /** + * Based on "Signed doubleword division from unsigned doubleword division" + * from Hacker's Delight, 2nd edition, figure 9-6. + */ + static constexpr std::pair divdi(const Int128& u, + const Int128& v) { + auto [q, r] = Uint128::udivdi(u.abs(), v.abs()); + + // If u and v have different signs, negate q. + // If is negative, negate r. + auto t = static_cast((u ^ v) >> 127); + auto s = static_cast(u >> 127); + return {static_cast((q ^ t) - t), static_cast((r ^ s) - s)}; + } + + public: + constexpr Int128() = default; + constexpr Int128(const Int128&) = default; + + explicit constexpr Int128(int64_t value) + : Int128(uint64_t(value), uint64_t(value >> 63)) {} + + /** + * Return the quotient and remainder of the division. + */ + constexpr std::pair divrem(const Int128& divisor) const { + return divdi(*this, divisor); + } + + /** + * Return the absolute value of this integer. + */ + constexpr Uint128 abs() const { + if (*this >= Int128{}) { + return Uint128{low, high}; + } + auto neg = -*this; + return Uint128{neg.low, neg.high}; + } + + constexpr bool operator==(const Int128& other) const { + return low == other.low && high == other.high; + } + + constexpr bool operator<(const Int128& other) const { + if (high == other.high) { + return low < other.low; + } + return int64_t(high) < int64_t(other.high); + } + + // Other operators are implemented in terms of operator== and operator<. + constexpr bool operator!=(const Int128& other) const { + return !(*this == other); + } + constexpr bool operator>(const Int128& other) const { return other < *this; } + constexpr bool operator<=(const Int128& other) const { + return !(other < *this); + } + constexpr bool operator>=(const Int128& other) const { + return !(*this < other); + } + + explicit constexpr operator bool() const { return !(*this == Int128{}); } + + explicit constexpr operator int8_t() const { return int8_t(low); } + explicit constexpr operator int16_t() const { return int16_t(low); } + explicit constexpr operator int32_t() const { return int32_t(low); } + explicit constexpr operator int64_t() const { return int64_t(low); } + + explicit constexpr operator uint8_t() const { return uint8_t(low); } + explicit constexpr operator uint16_t() const { return uint16_t(low); } + explicit constexpr operator uint32_t() const { return uint32_t(low); } + explicit constexpr operator uint64_t() const { return uint64_t(low); } + + explicit constexpr operator Uint128() const { return Uint128{low, high}; } + + explicit operator double() const { + return Uint128::toDouble(abs(), *this < Int128{0}); + } + + constexpr Int128 operator+(const Int128& other) const { + return Int128{Uint128{*this} + Uint128{other}}; + } + + constexpr Int128 operator-(const Int128& other) const { + return Int128{Uint128{*this} - Uint128{other}}; + } + + constexpr Int128 operator*(const Int128& other) const { + return Int128{Uint128{*this} * Uint128{other}}; + } + + constexpr Int128 operator/(const Int128& other) const { + auto [quot, rem] = divrem(other); + return quot; + } + + constexpr Int128 operator%(const Int128& other) const { + auto [quot, rem] = divrem(other); + return rem; + } + + constexpr Int128 operator<<(int shift) const { + return Int128{Uint128{*this} << shift}; + } + + constexpr Int128 operator>>(int shift) const { + MOZ_ASSERT(0 <= shift && shift <= 127, "undefined shift amount"); + + // Ensure the shift amount is in range. + shift &= 127; + + // "§2-17 Double-Length Shifts" from Hacker's Delight, 2nd edition. + if (shift >= 64) { + uint64_t y0 = uint64_t(int64_t(high) >> (shift - 64)); + uint64_t y1 = uint64_t(int64_t(high) >> 63); + return Int128{y0, y1}; + } + uint64_t y0 = low >> shift | high << (64 - shift); + uint64_t y1 = uint64_t(int64_t(high) >> shift); + return Int128{y0, y1}; + } + + constexpr Int128 operator&(const Int128& other) const { + return Int128{low & other.low, high & other.high}; + } + + constexpr Int128 operator|(const Int128& other) const { + return Int128{low | other.low, high | other.high}; + } + + constexpr Int128 operator^(const Int128& other) const { + return Int128{low ^ other.low, high ^ other.high}; + } + + constexpr Int128 operator~() const { return Int128{~low, ~high}; } + + constexpr Int128 operator-() const { return Int128{} - *this; } + + constexpr Int128 operator+() const { return *this; } + + constexpr Int128& operator++() { + *this = *this + Int128{1, 0}; + return *this; + } + + constexpr Int128 operator++(int) { + auto result = *this; + ++*this; + return result; + } + + constexpr Int128& operator--() { + *this = *this - Int128{1, 0}; + return *this; + } + + constexpr Int128 operator--(int) { + auto result = *this; + --*this; + return result; + } + + constexpr Int128 operator+=(const Int128& other) { + *this = *this + other; + return *this; + } + + constexpr Int128 operator-=(const Int128& other) { + *this = *this - other; + return *this; + } + + constexpr Int128 operator*=(const Int128& other) { + *this = *this * other; + return *this; + } + + constexpr Int128 operator/=(const Int128& other) { + *this = *this / other; + return *this; + } + + constexpr Int128 operator%=(const Int128& other) { + *this = *this % other; + return *this; + } + + constexpr Int128 operator&=(const Int128& other) { + *this = *this & other; + return *this; + } + + constexpr Int128 operator|=(const Int128& other) { + *this = *this | other; + return *this; + } + + constexpr Int128 operator^=(const Int128& other) { + *this = *this ^ other; + return *this; + } + + constexpr Int128 operator<<=(int shift) { + *this = *this << shift; + return *this; + } + + constexpr Int128 operator>>=(int shift) { + *this = *this >> shift; + return *this; + } +}; + +constexpr Uint128::operator Int128() const { return Int128{low, high}; } + +} /* namespace js::temporal */ + +template <> +class std::numeric_limits { + public: + static constexpr bool is_specialized = true; + static constexpr bool is_signed = true; + static constexpr bool is_integer = true; + static constexpr bool is_exact = true; + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr std::float_denorm_style has_denorm = std::denorm_absent; + static constexpr bool has_denorm_loss = false; + static constexpr std::float_round_style round_style = std::round_toward_zero; + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = true; + static constexpr int digits = CHAR_BIT * sizeof(js::temporal::Int128) - 1; + static constexpr int digits10 = int(digits * /* std::log10(2) */ 0.30102999); + static constexpr int max_digits10 = 0; + static constexpr int radix = 2; + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + static constexpr bool traps = true; + static constexpr bool tinyness_before = false; + + static constexpr auto min() noexcept { + return js::temporal::Int128{1} << 127; + } + static constexpr auto lowest() noexcept { return min(); } + static constexpr auto max() noexcept { return ~min(); } + static constexpr auto epsilon() noexcept { return js::temporal::Int128{}; } + static constexpr auto round_error() noexcept { + return js::temporal::Int128{}; + } + static constexpr auto infinity() noexcept { return js::temporal::Int128{}; } + static constexpr auto quiet_NaN() noexcept { return js::temporal::Int128{}; } + static constexpr auto signaling_NaN() noexcept { + return js::temporal::Int128{}; + } + static constexpr auto denorm_min() noexcept { return js::temporal::Int128{}; } +}; + +template <> +class std::numeric_limits { + public: + static constexpr bool is_specialized = true; + static constexpr bool is_signed = false; + static constexpr bool is_integer = true; + static constexpr bool is_exact = true; + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr std::float_denorm_style has_denorm = std::denorm_absent; + static constexpr bool has_denorm_loss = false; + static constexpr std::float_round_style round_style = std::round_toward_zero; + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = true; + static constexpr int digits = CHAR_BIT * sizeof(js::temporal::Uint128); + static constexpr int digits10 = int(digits * /* std::log10(2) */ 0.30102999); + static constexpr int max_digits10 = 0; + static constexpr int radix = 2; + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + static constexpr bool traps = true; + static constexpr bool tinyness_before = false; + + static constexpr auto min() noexcept { return js::temporal::Uint128{}; } + static constexpr auto lowest() noexcept { return min(); } + static constexpr auto max() noexcept { return ~js::temporal::Uint128{}; } + static constexpr auto epsilon() noexcept { return js::temporal::Uint128{}; } + static constexpr auto round_error() noexcept { + return js::temporal::Uint128{}; + } + static constexpr auto infinity() noexcept { return js::temporal::Uint128{}; } + static constexpr auto quiet_NaN() noexcept { return js::temporal::Uint128{}; } + static constexpr auto signaling_NaN() noexcept { + return js::temporal::Uint128{}; + } + static constexpr auto denorm_min() noexcept { + return js::temporal::Uint128{}; + } +}; + +#endif /* builtin_temporal_Int128_h */ diff --git a/js/src/builtin/temporal/Int96.cpp b/js/src/builtin/temporal/Int96.cpp index 73ea5ce90e..984271566a 100644 --- a/js/src/builtin/temporal/Int96.cpp +++ b/js/src/builtin/temporal/Int96.cpp @@ -39,12 +39,12 @@ mozilla::Maybe Int96::fromInteger(double value) { // Inlined version of |BigInt::createFromDouble()| for DigitBits=32. See the // comments in |BigInt::createFromDouble()| for how this code works. - constexpr size_t DigitBits = 32; + constexpr int DigitBits = 32; // The number can't have more than three digits when it's below |maximum|. Int96::Digits digits = {}; - int exponent = mozilla::ExponentComponent(value); + int exponent = int(mozilla::ExponentComponent(value)); MOZ_ASSERT(0 <= exponent && exponent <= 95, "exponent is lower than exponent of 0x1p+96"); @@ -62,7 +62,7 @@ mozilla::Maybe Int96::fromInteger(double value) { int msdTopBit = exponent % DigitBits; // First, build the MSD by shifting the mantissa appropriately. - int remainingMantissaBits = Double::kSignificandWidth - msdTopBit; + int remainingMantissaBits = int(Double::kSignificandWidth - msdTopBit); digits[--length] = mantissa >> remainingMantissaBits; // Fill in digits containing mantissa contributions. diff --git a/js/src/builtin/temporal/Int96.h b/js/src/builtin/temporal/Int96.h index dfb0a5c231..36ed359eca 100644 --- a/js/src/builtin/temporal/Int96.h +++ b/js/src/builtin/temporal/Int96.h @@ -133,7 +133,7 @@ class Int96 final { remainder = n % divisor; } - int64_t result = (TwoDigit(quotient[1]) << 32) | quotient[0]; + int64_t result = int64_t((TwoDigit(quotient[1]) << 32) | quotient[0]); if (negative) { result *= -1; if (remainder != 0) { diff --git a/js/src/builtin/temporal/PlainDate.cpp b/js/src/builtin/temporal/PlainDate.cpp index a4ad0e418f..a15d95cdc1 100644 --- a/js/src/builtin/temporal/PlainDate.cpp +++ b/js/src/builtin/temporal/PlainDate.cpp @@ -7,6 +7,7 @@ #include "builtin/temporal/PlainDate.h" #include "mozilla/Assertions.h" +#include "mozilla/Casting.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" @@ -99,20 +100,15 @@ static bool IsValidISODate(T year, T month, T day) { // Step 3. int32_t daysInMonth = js::temporal::ISODaysInMonth(year, int32_t(month)); - // Step 4. - if (day < 1 || day > daysInMonth) { - return false; - } - - // Step 5. - return true; + // Steps 4-5. + return 1 <= day && day <= daysInMonth; } /** * IsValidISODate ( year, month, day ) */ bool js::temporal::IsValidISODate(const PlainDate& date) { - auto& [year, month, day] = date; + const auto& [year, month, day] = date; return ::IsValidISODate(year, month, day); } @@ -170,20 +166,15 @@ static bool ThrowIfInvalidISODate(JSContext* cx, T year, T month, T day) { // Step 3. int32_t daysInMonth = js::temporal::ISODaysInMonth(year, int32_t(month)); - // Step 4. - if (!ThrowIfInvalidDateValue(cx, "day", 1, daysInMonth, day)) { - return false; - } - - // Step 5. - return true; + // Steps 4-5. + return ThrowIfInvalidDateValue(cx, "day", 1, daysInMonth, day); } /** * IsValidISODate ( year, month, day ) */ bool js::temporal::ThrowIfInvalidISODate(JSContext* cx, const PlainDate& date) { - auto& [year, month, day] = date; + const auto& [year, month, day] = date; return ::ThrowIfInvalidISODate(cx, year, month, day); } @@ -201,7 +192,7 @@ bool js::temporal::ThrowIfInvalidISODate(JSContext* cx, double year, * With |overflow = "constrain"|. */ static PlainDate ConstrainISODate(const PlainDate& date) { - auto& [year, month, day] = date; + const auto& [year, month, day] = date; // Step 1.a. int32_t m = std::clamp(month, 1, 12); @@ -316,13 +307,16 @@ static PlainDateObject* CreateTemporalDate(JSContext* cx, const CallArgs& args, } // Step 5. - object->setFixedSlot(PlainDateObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + object->setFixedSlot(PlainDateObject::ISO_YEAR_SLOT, + Int32Value(int32_t(isoYear))); // Step 6. - object->setFixedSlot(PlainDateObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + object->setFixedSlot(PlainDateObject::ISO_MONTH_SLOT, + Int32Value(int32_t(isoMonth))); // Step 7. - object->setFixedSlot(PlainDateObject::ISO_DAY_SLOT, Int32Value(isoDay)); + object->setFixedSlot(PlainDateObject::ISO_DAY_SLOT, + Int32Value(int32_t(isoDay))); // Step 8. object->setFixedSlot(PlainDateObject::CALENDAR_SLOT, calendar.toValue()); @@ -336,7 +330,7 @@ static PlainDateObject* CreateTemporalDate(JSContext* cx, const CallArgs& args, */ PlainDateObject* js::temporal::CreateTemporalDate( JSContext* cx, const PlainDate& date, Handle calendar) { - auto& [isoYear, isoMonth, isoDay] = date; + const auto& [isoYear, isoMonth, isoDay] = date; // Step 1. if (!ThrowIfInvalidISODate(cx, date)) { @@ -602,30 +596,28 @@ bool js::temporal::ToTemporalDate(JSContext* cx, Handle item, /** * Mathematical Operations, "modulo" notation. */ -static int32_t NonNegativeModulo(double x, int32_t y) { - MOZ_ASSERT(IsInteger(x)); +static int32_t NonNegativeModulo(int64_t x, int32_t y) { MOZ_ASSERT(y > 0); - double r = std::fmod(x, y); - - int32_t result; - MOZ_ALWAYS_TRUE(mozilla::NumberEqualsInt32(r, &result)); - + int32_t result = mozilla::AssertedCast(x % y); return (result < 0) ? (result + y) : result; } struct BalancedYearMonth final { - double year = 0; + int64_t year = 0; int32_t month = 0; }; /** * BalanceISOYearMonth ( year, month ) */ -static BalancedYearMonth BalanceISOYearMonth(double year, double month) { - // Step 1. - MOZ_ASSERT(IsInteger(year)); - MOZ_ASSERT(IsInteger(month)); +static BalancedYearMonth BalanceISOYearMonth(int64_t year, int64_t month) { + MOZ_ASSERT(std::abs(year) < (int64_t(1) << 33), + "year is the addition of plain-date year with duration years"); + MOZ_ASSERT(std::abs(month) < (int64_t(1) << 33), + "month is the addition of plain-date month with duration months"); + + // Step 1. (Not applicable in our implementation.) // Note: If either abs(year) or abs(month) is greater than 2^53 (the double // integral precision limit), the additions resp. subtractions below are @@ -633,18 +625,17 @@ static BalancedYearMonth BalanceISOYearMonth(double year, double month) { // function (AddISODate) will throw an error for large values anyway. // Step 2. - year = year + std::floor((month - 1) / 12); - MOZ_ASSERT(IsInteger(year) || std::isinf(year)); + int64_t balancedYear = year + temporal::FloorDiv(month - 1, 12); // Step 3. - int32_t mon = NonNegativeModulo(month - 1, 12) + 1; - MOZ_ASSERT(1 <= mon && mon <= 12); + int32_t balancedMonth = NonNegativeModulo(month - 1, 12) + 1; + MOZ_ASSERT(1 <= balancedMonth && balancedMonth <= 12); // Step 4. - return {year, mon}; + return {balancedYear, balancedMonth}; } -static bool CanBalanceISOYear(double year) { +static bool CanBalanceISOYear(int64_t year) { // TODO: Export these values somewhere. constexpr int32_t minYear = -271821; constexpr int32_t maxYear = 275760; @@ -654,7 +645,7 @@ static bool CanBalanceISOYear(double year) { return minYear <= year && year <= maxYear; } -static bool CanBalanceISODay(double day) { +static bool CanBalanceISODay(int64_t day) { // The maximum number of seconds from the epoch is 8.64 * 10^12. constexpr int64_t maxInstantSeconds = 8'640'000'000'000; @@ -684,10 +675,9 @@ PlainDate js::temporal::BalanceISODateNew(int32_t year, int32_t month, MOZ_ASSERT(1 <= month && month <= 12); // Steps 1-3. - int64_t ms = MakeDate(year, month, day); + double ms = double(MakeDate(year, month, day)); - // FIXME: spec issue - |ms| can be non-finite - // https://github.com/tc39/proposal-temporal/issues/2315 + // TODO: Add ISODateToEpochDays & friends which handle larger inputs. // TODO: This approach isn't efficient, because MonthFromTime and DayFromTime // both recompute YearFromTime. @@ -700,15 +690,19 @@ PlainDate js::temporal::BalanceISODateNew(int32_t year, int32_t month, /** * BalanceISODate ( year, month, day ) */ -bool js::temporal::BalanceISODate(JSContext* cx, int32_t year, int32_t month, - int64_t day, PlainDate* result) { +bool js::temporal::BalanceISODate(JSContext* cx, const PlainDate& date, + int64_t days, PlainDate* result) { + MOZ_ASSERT(IsValidISODate(date)); + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + int64_t day = int64_t(date.day) + days; if (!CanBalanceISODay(day)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PLAIN_DATE_INVALID); return false; } - *result = BalanceISODate(year, month, int32_t(day)); + *result = BalanceISODate(date.year, date.month, int32_t(day)); return true; } @@ -725,9 +719,8 @@ PlainDate js::temporal::BalanceISODate(int32_t year, int32_t month, MOZ_ASSERT(1 <= month && month <= 12); MOZ_ASSERT(CanBalanceISODay(day)); - // TODO: BalanceISODate now works using MakeDate + // TODO: BalanceISODate now works using ISODateToEpochDays & friends. // TODO: Can't use JS::MakeDate, because it expects valid month/day values. - // https://github.com/tc39/proposal-temporal/issues/2315 // Step 1. (Not applicable in our implementation.) @@ -811,7 +804,7 @@ PlainDate js::temporal::BalanceISODate(int32_t year, int32_t month, * AddISODate ( year, month, day, years, months, weeks, days, overflow ) */ bool js::temporal::AddISODate(JSContext* cx, const PlainDate& date, - const Duration& duration, + const DateDuration& duration, TemporalOverflow overflow, PlainDate* result) { MOZ_ASSERT(IsValidISODate(date)); MOZ_ASSERT(ISODateTimeWithinLimits(date)); @@ -822,18 +815,11 @@ bool js::temporal::AddISODate(JSContext* cx, const PlainDate& date, // BalanceISODate. MOZ_ASSERT(IsValidDuration(duration)); - // Step 1. - MOZ_ASSERT(IsInteger(duration.years)); - MOZ_ASSERT(IsInteger(duration.months)); - MOZ_ASSERT(IsInteger(duration.weeks)); - MOZ_ASSERT(IsInteger(duration.days)); - - // Step 2. (Not applicable in our implementation.) + // Steps 1-2. (Not applicable in our implementation.) // Step 3. auto yearMonth = BalanceISOYearMonth(date.year + duration.years, date.month + duration.months); - MOZ_ASSERT(IsInteger(yearMonth.year) || std::isinf(yearMonth.year)); MOZ_ASSERT(1 <= yearMonth.month && yearMonth.month <= 12); // FIXME: spec issue? @@ -881,7 +867,7 @@ bool js::temporal::AddISODate(JSContext* cx, const PlainDate& date, // about imprecise number arithmetic here. // Steps 5-6. - double d = regulated.day + (duration.days + duration.weeks * 7); + int64_t d = regulated.day + (duration.days + duration.weeks * 7); // Just as with |yearMonth.year|, also directly throw an error if the |days| // value is too large. @@ -952,65 +938,51 @@ static bool HasYearsMonthsOrWeeks(const Duration& duration) { return duration.years != 0 || duration.months != 0 || duration.weeks != 0; } -static bool AddDate(JSContext* cx, const PlainDate& date, - const Duration& duration, Handle maybeOptions, - PlainDate* result) { - MOZ_ASSERT(!HasYearsMonthsOrWeeks(duration)); +static bool HasYearsMonthsOrWeeks(const DateDuration& duration) { + return duration.years != 0 || duration.months != 0 || duration.weeks != 0; +} - // Steps 1-3. (Not applicable) +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +static bool AddDate(JSContext* cx, const PlainDate& date, + const NormalizedDuration& duration, + TemporalOverflow overflow, PlainDate* result) { + MOZ_ASSERT(!HasYearsMonthsOrWeeks(duration.date)); + MOZ_ASSERT(IsValidDuration(duration)); - // Step 4. - auto overflow = TemporalOverflow::Constrain; - if (maybeOptions) { - if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { - return false; - } - } + // Steps 1-4. (Not applicable) - // Step 5. - TimeDuration daysDuration; - if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &daysDuration)) { - return false; - } + // Step 5. (Not applicable) + const auto& timeDuration = duration.time; // Step 6. - return AddISODate(cx, date, {0, 0, 0, daysDuration.days}, overflow, result); + int64_t balancedDays = + BalanceTimeDuration(timeDuration, TemporalUnit::Day).days; + int64_t days = duration.date.days + balancedDays; + + // Step 7. + return AddISODate(cx, date, {0, 0, 0, days}, overflow, result); } static bool AddDate(JSContext* cx, Handle> date, - const Duration& duration, Handle maybeOptions, - PlainDate* result) { + const NormalizedDuration& duration, + TemporalOverflow overflow, PlainDate* result) { auto* unwrappedDate = date.unwrap(cx); if (!unwrappedDate) { return false; } - return ::AddDate(cx, ToPlainDate(unwrappedDate), duration, maybeOptions, - result); -} - -static PlainDateObject* AddDate(JSContext* cx, Handle calendar, - Handle> date, - const Duration& duration, - Handle maybeOptions) { - // Steps 1-3. (Not applicable) - - // Steps 4-6. - PlainDate resultDate; - if (!::AddDate(cx, date, duration, maybeOptions, &resultDate)) { - return nullptr; - } - - // Step 7. - return CreateTemporalDate(cx, resultDate, calendar.receiver()); + return ::AddDate(cx, ToPlainDate(unwrappedDate), duration, overflow, result); } /** * AddDate ( calendarRec, plainDate, duration [ , options ] ) */ -Wrapped js::temporal::AddDate( - JSContext* cx, Handle calendar, - Handle> date, const Duration& duration, - Handle options) { +static Wrapped AddDate(JSContext* cx, + Handle calendar, + Handle> date, + const Duration& duration, + Handle options) { // Step 1. MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); @@ -1022,8 +994,23 @@ Wrapped js::temporal::AddDate( return temporal::CalendarDateAdd(cx, calendar, date, duration, options); } - // Steps 4-7. - return ::AddDate(cx, calendar, date, duration, options); + // Step 4. + auto overflow = TemporalOverflow::Constrain; + if (!ToTemporalOverflow(cx, options, &overflow)) { + return nullptr; + } + + // Step 5. + auto normalized = CreateNormalizedDurationRecord(duration); + + // Steps 6-7. + PlainDate resultDate; + if (!::AddDate(cx, date, normalized, overflow, &resultDate)) { + return nullptr; + } + + // Step 8. + return CreateTemporalDate(cx, resultDate, calendar.receiver()); } /** @@ -1031,7 +1018,7 @@ Wrapped js::temporal::AddDate( */ Wrapped js::temporal::AddDate( JSContext* cx, Handle calendar, - Handle> date, const Duration& duration) { + Handle> date, const DateDuration& duration) { // Step 1. MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); @@ -1043,8 +1030,20 @@ Wrapped js::temporal::AddDate( return CalendarDateAdd(cx, calendar, date, duration); } - // Steps 4-7. - return ::AddDate(cx, calendar, date, duration, nullptr); + // Step 4. + auto overflow = TemporalOverflow::Constrain; + + // Step 5. + auto normalized = NormalizedDuration{duration}; + + // Steps 6-7. + PlainDate resultDate; + if (!::AddDate(cx, date, normalized, overflow, &resultDate)) { + return nullptr; + } + + // Step 8. + return CreateTemporalDate(cx, resultDate, calendar.receiver()); } /** @@ -1052,33 +1051,15 @@ Wrapped js::temporal::AddDate( */ Wrapped js::temporal::AddDate( JSContext* cx, Handle calendar, - Handle> date, - Handle> durationObj) { - auto* unwrappedDuration = durationObj.unwrap(cx); - if (!unwrappedDuration) { - return nullptr; - } - auto duration = ToDuration(unwrappedDuration); - - // Step 1. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); - - // Step 2. (Not applicable in our implementation.) - - // Step 3. - if (HasYearsMonthsOrWeeks(duration)) { - return CalendarDateAdd(cx, calendar, date, durationObj); - } - - // Steps 4-7. - return ::AddDate(cx, calendar, date, duration, nullptr); + Handle> date, const DateDuration& duration, + Handle options) { + return ::AddDate(cx, calendar, date, duration.toDuration(), options); } /** * AddDate ( calendarRec, plainDate, duration [ , options ] ) */ -Wrapped js::temporal::AddDate( +static Wrapped AddDate( JSContext* cx, Handle calendar, Handle> date, Handle> durationObj, Handle options) { @@ -1099,15 +1080,30 @@ Wrapped js::temporal::AddDate( return temporal::CalendarDateAdd(cx, calendar, date, durationObj, options); } - // Steps 4-7. - return ::AddDate(cx, calendar, date, duration, options); + // Step 4. + auto overflow = TemporalOverflow::Constrain; + if (!ToTemporalOverflow(cx, options, &overflow)) { + return nullptr; + } + + // Step 5. + auto normalized = CreateNormalizedDurationRecord(duration); + + // Steps 6-7. + PlainDate resultDate; + if (!::AddDate(cx, date, normalized, overflow, &resultDate)) { + return nullptr; + } + + // Step 8. + return CreateTemporalDate(cx, resultDate, calendar.receiver()); } /** * AddDate ( calendarRec, plainDate, duration [ , options ] ) */ bool js::temporal::AddDate(JSContext* cx, Handle calendar, - const PlainDate& date, const Duration& duration, + const PlainDate& date, const DateDuration& duration, Handle options, PlainDate* result) { // Step 1. MOZ_ASSERT( @@ -1121,8 +1117,17 @@ bool js::temporal::AddDate(JSContext* cx, Handle calendar, result); } - // Steps 4-7. - return ::AddDate(cx, date, duration, options, result); + // Step 4. + auto overflow = TemporalOverflow::Constrain; + if (!ToTemporalOverflow(cx, options, &overflow)) { + return false; + } + + // Step 5. + auto normalized = NormalizedDuration{duration}; + + // Steps 5-8. + return ::AddDate(cx, date, normalized, overflow, result); } /** @@ -1130,7 +1135,7 @@ bool js::temporal::AddDate(JSContext* cx, Handle calendar, */ bool js::temporal::AddDate(JSContext* cx, Handle calendar, Handle> date, - const Duration& duration, PlainDate* result) { + const DateDuration& duration, PlainDate* result) { // Step 1. MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); @@ -1142,8 +1147,14 @@ bool js::temporal::AddDate(JSContext* cx, Handle calendar, return CalendarDateAdd(cx, calendar, date, duration, result); } - // Steps 4-7. - return ::AddDate(cx, date, duration, nullptr, result); + // Step 4. + auto overflow = TemporalOverflow::Constrain; + + // Step 5. + auto normalized = NormalizedDuration{duration}; + + // Steps 6-8. + return ::AddDate(cx, date, normalized, overflow, result); } /** @@ -1153,8 +1164,9 @@ bool js::temporal::DifferenceDate(JSContext* cx, Handle calendar, Handle> one, Handle> two, + TemporalUnit largestUnit, Handle options, - Duration* result) { + DateDuration* result) { auto* unwrappedOne = one.unwrap(cx); if (!unwrappedOne) { return false; @@ -1172,8 +1184,7 @@ bool js::temporal::DifferenceDate(JSContext* cx, // Step 3. MOZ_ASSERT(options->staticPrototype() == nullptr); - // Step 4. - MOZ_ASSERT(options->containsPure(cx->names().largestUnit)); + // Step 4. (Not applicable in our implementation.) // Step 5. if (oneDate == twoDate) { @@ -1182,30 +1193,18 @@ bool js::temporal::DifferenceDate(JSContext* cx, } // Step 6. - Rooted largestUnit(cx); - if (!GetProperty(cx, options, options, cx->names().largestUnit, - &largestUnit)) { - return false; - } - - if (largestUnit.isString()) { - bool isDay; - if (!EqualStrings(cx, largestUnit.toString(), cx->names().day, &isDay)) { - return false; - } - - if (isDay) { - // Step 6.a. - int32_t days = DaysUntil(oneDate, twoDate); + if (largestUnit == TemporalUnit::Day) { + // Step 6.a. + int32_t days = DaysUntil(oneDate, twoDate); - // Step 6.b. - *result = {0, 0, 0, double(days)}; - return true; - } + // Step 6.b. + *result = {0, 0, 0, days}; + return true; } // Step 7. - return CalendarDateUntil(cx, calendar, one, two, options, result); + return CalendarDateUntil(cx, calendar, one, two, largestUnit, options, + result); } /** @@ -1215,7 +1214,8 @@ bool js::temporal::DifferenceDate(JSContext* cx, Handle calendar, Handle> one, Handle> two, - TemporalUnit largestUnit, Duration* result) { + TemporalUnit largestUnit, + DateDuration* result) { auto* unwrappedOne = one.unwrap(cx); if (!unwrappedOne) { return false; @@ -1242,7 +1242,69 @@ bool js::temporal::DifferenceDate(JSContext* cx, int32_t days = DaysUntil(oneDate, twoDate); // Step 6.b. - *result = {0, 0, 0, double(days)}; + *result = {0, 0, 0, days}; + return true; + } + + // Step 7. + return CalendarDateUntil(cx, calendar, one, two, largestUnit, result); +} + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool js::temporal::DifferenceDate(JSContext* cx, + Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, + Handle options, + DateDuration* result) { + // Steps 1-4. (Not applicable in our implementation.) + + // Step 5. + if (one == two) { + *result = {}; + return true; + } + + // Step 6. + if (largestUnit == TemporalUnit::Day) { + // Step 6.a. + int32_t days = DaysUntil(one, two); + + // Step 6.b. + *result = {0, 0, 0, days}; + return true; + } + + // Step 7. + return CalendarDateUntil(cx, calendar, one, two, largestUnit, options, + result); +} + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool js::temporal::DifferenceDate(JSContext* cx, + Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, + DateDuration* result) { + // Steps 1-4. (Not applicable in our implementation.) + + // Step 5. + if (one == two) { + *result = {}; + return true; + } + + // Step 6. + if (largestUnit == TemporalUnit::Day) { + // Step 6.a. + int32_t days = DaysUntil(one, two); + + // Step 6.b. + *result = {0, 0, 0, days}; return true; } @@ -1280,8 +1342,8 @@ int32_t js::temporal::CompareISODate(const PlainDate& one, static DateDuration CreateDateDurationRecord(int32_t years, int32_t months, int32_t weeks, int32_t days) { MOZ_ASSERT(IsValidDuration( - {double(years), double(months), double(weeks), double(days)})); - return {double(years), double(months), double(weeks), double(days)}; + Duration{double(years), double(months), double(weeks), double(days)})); + return {years, months, weeks, days}; } /** @@ -1577,31 +1639,19 @@ static bool DifferenceTemporalPlainDate(JSContext* cx, } // Steps 8-9. - Duration duration; + DateDuration difference; if (resolvedOptions) { - // Step 8. - Rooted largestUnitValue( - cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); - if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, - largestUnitValue)) { - return false; - } - - // Step 9. - Duration result; - if (!DifferenceDate(cx, calendar, temporalDate, other, resolvedOptions, - &result)) { + // Steps 8-9. + if (!DifferenceDate(cx, calendar, temporalDate, other, settings.largestUnit, + resolvedOptions, &difference)) { return false; } - duration = result.date(); } else { // Steps 8-9. - Duration result; if (!DifferenceDate(cx, calendar, temporalDate, other, settings.largestUnit, - &result)) { + &difference)) { return false; } - duration = result.date(); } // Step 10. @@ -1611,8 +1661,8 @@ static bool DifferenceTemporalPlainDate(JSContext* cx, // Step 11. if (!roundingGranularityIsNoop) { // Steps 11.a-b. - Duration roundResult; - if (!temporal::RoundDuration(cx, duration.date(), + NormalizedDuration roundResult; + if (!temporal::RoundDuration(cx, {difference, {}}, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, temporalDate, calendar, &roundResult)) { @@ -1622,19 +1672,21 @@ static bool DifferenceTemporalPlainDate(JSContext* cx, // Step 11.c. DateDuration balanceResult; if (!temporal::BalanceDateDurationRelative( - cx, roundResult.date(), settings.largestUnit, settings.smallestUnit, + cx, roundResult.date, settings.largestUnit, settings.smallestUnit, temporalDate, calendar, &balanceResult)) { return false; } - duration = balanceResult.toDuration(); + difference = balanceResult; } // Step 12. + auto duration = difference.toDuration(); if (operation == TemporalDifference::Since) { duration = duration.negate(); } + MOZ_ASSERT(IsValidDuration(duration)); - auto* obj = CreateTemporalDuration(cx, duration.date()); + auto* obj = CreateTemporalDuration(cx, duration); if (!obj) { return false; } @@ -2235,14 +2287,14 @@ static bool PlainDate_toPlainDateTime(JSContext* cx, const CallArgs& args) { // Default initialize the time component to all zero. PlainDateTime dateTime = {ToPlainDate(temporalDate), {}}; - // Step 4. (Reordered) + // Step 3. (Inlined ToTemporalTimeOrMidnight) if (args.hasDefined(0)) { if (!ToTemporalTime(cx, args[0], &dateTime.time)) { return false; } } - // Steps 3 and 5. + // Step 4. auto* obj = CreateTemporalDateTime(cx, dateTime, calendar); if (!obj) { return false; @@ -2433,8 +2485,7 @@ static bool PlainDate_subtract(JSContext* cx, const CallArgs& args) { } // Step 7. - auto result = - temporal::AddDate(cx, calendar, temporalDate, negatedDuration, options); + auto result = ::AddDate(cx, calendar, temporalDate, negatedDuration, options); if (!result) { return false; } @@ -2465,13 +2516,11 @@ static bool PlainDate_with(JSContext* cx, const CallArgs& args) { if (!temporalDateLike) { return false; } - - // Step 4. - if (!RejectTemporalLikeObject(cx, temporalDateLike)) { + if (!ThrowIfTemporalLikeObject(cx, temporalDateLike)) { return false; } - // Step 5. + // Step 4. Rooted resolvedOptions(cx); if (args.hasDefined(1)) { Rooted options(cx, @@ -2487,7 +2536,7 @@ static bool PlainDate_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 6. + // Step 5. Rooted calendarValue(cx, temporalDate->calendar()); Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, calendarValue, @@ -2500,7 +2549,7 @@ static bool PlainDate_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 7. + // Step 6. JS::RootedVector fieldNames(cx); if (!CalendarFields(cx, calendar, {CalendarField::Day, CalendarField::Month, @@ -2509,34 +2558,34 @@ static bool PlainDate_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 8. + // Step 7. Rooted fields( cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); if (!fields) { return false; } - // Step 9. + // Step 8. Rooted partialDate( cx, PreparePartialTemporalFields(cx, temporalDateLike, fieldNames)); if (!partialDate) { return false; } - // Step 10. + // Step 9. Rooted mergedFields( cx, CalendarMergeFields(cx, calendar, fields, partialDate)); if (!mergedFields) { return false; } - // Step 11. + // Step 10. fields = PrepareTemporalFields(cx, mergedFields, fieldNames); if (!fields) { return false; } - // Step 12. + // Step 11. auto result = temporal::CalendarDateFromFields(cx, calendar, fields, resolvedOptions); if (!result) { @@ -2677,7 +2726,7 @@ static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { // Steps 3-4 Rooted timeZone(cx); - Rooted temporalTime(cx); + PlainTime time = {}; if (args.get(0).isObject()) { Rooted item(cx, &args[0].toObject()); @@ -2686,8 +2735,7 @@ static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { // Step 3.a.i. timeZone.set(TimeZoneValue(item)); - // Step 3.a.ii. - temporalTime.setUndefined(); + // Step 3.a.ii. (Not applicable in our implementation.) } else { // Step 3.b.i. Rooted timeZoneLike(cx); @@ -2702,8 +2750,7 @@ static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { return false; } - // Step 3.b.ii.2. - temporalTime.setUndefined(); + // Step 3.b.ii.2. (Not applicable in our implementation.) } else { // Step 3.b.iii.1. if (!ToTemporalTimeZone(cx, timeZoneLike, &timeZone)) { @@ -2711,10 +2758,18 @@ static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { } // Step 3.b.iii.2. + Rooted temporalTime(cx); if (!GetProperty(cx, item, item, cx->names().plainTime, &temporalTime)) { return false; } + + // Step 5. (Inlined ToTemporalTimeOrMidnight) + if (!temporalTime.isUndefined()) { + if (!ToTemporalTime(cx, temporalTime, &time)) { + return false; + } + } } } } else { @@ -2723,19 +2778,12 @@ static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { return false; } - // Step 4.b. - temporalTime.setUndefined(); + // Step 4.b. (Not applicable in our implementation.) } - // Step 6.a. - PlainTime time = {}; - if (!temporalTime.isUndefined()) { - if (!ToTemporalTime(cx, temporalTime, &time)) { - return false; - } - } + // Step 5. (Moved next to step 3.b.iii.2.) - // Steps 5.a and 6.b + // Step 6. Rooted temporalDateTime(cx); if (!CreateTemporalDateTime(cx, {date, time}, calendar, &temporalDateTime)) { return false; diff --git a/js/src/builtin/temporal/PlainDate.h b/js/src/builtin/temporal/PlainDate.h index 75a3a3f2a1..f8217eb5e0 100644 --- a/js/src/builtin/temporal/PlainDate.h +++ b/js/src/builtin/temporal/PlainDate.h @@ -8,6 +8,7 @@ #define builtin_temporal_PlainDate_h #include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" #include #include @@ -60,7 +61,7 @@ class PlainDateObject : public NativeObject { static const ClassSpec classSpec_; }; -class PlainDateWithCalendar { +class MOZ_STACK_CLASS PlainDateWithCalendar final { PlainDate date_; CalendarValue calendar_; @@ -150,9 +151,9 @@ bool RegulateISODate(JSContext* cx, const PlainDate& date, TemporalOverflow overflow, PlainDate* result); struct RegulatedISODate final { - double year; - int32_t month; - int32_t day; + double year = 0; + int32_t month = 0; + int32_t day = 0; }; /** @@ -164,8 +165,9 @@ bool RegulateISODate(JSContext* cx, double year, double month, double day, /** * AddISODate ( year, month, day, years, months, weeks, days, overflow ) */ -bool AddISODate(JSContext* cx, const PlainDate& date, const Duration& duration, - TemporalOverflow overflow, PlainDate* result); +bool AddISODate(JSContext* cx, const PlainDate& date, + const DateDuration& duration, TemporalOverflow overflow, + PlainDate* result); /** * AddDate ( calendarRec, plainDate, duration [ , options ] ) @@ -173,8 +175,7 @@ bool AddISODate(JSContext* cx, const PlainDate& date, const Duration& duration, Wrapped AddDate(JSContext* cx, JS::Handle calendar, JS::Handle> date, - const Duration& duration, - JS::Handle options); + const DateDuration& duration); /** * AddDate ( calendarRec, plainDate, duration [ , options ] ) @@ -182,30 +183,14 @@ Wrapped AddDate(JSContext* cx, Wrapped AddDate(JSContext* cx, JS::Handle calendar, JS::Handle> date, - const Duration& duration); - -/** - * AddDate ( calendarRec, plainDate, duration [ , options ] ) - */ -Wrapped AddDate( - JSContext* cx, JS::Handle calendar, - JS::Handle> date, - JS::Handle> durationObj, - JS::Handle options); - -/** - * AddDate ( calendarRec, plainDate, duration [ , options ] ) - */ -Wrapped AddDate( - JSContext* cx, JS::Handle calendar, - JS::Handle> date, - JS::Handle> durationObj); + const DateDuration& duration, + JS::Handle options); /** * AddDate ( calendarRec, plainDate, duration [ , options ] ) */ bool AddDate(JSContext* cx, JS::Handle calendar, - const PlainDate& date, const Duration& duration, + const PlainDate& date, const DateDuration& duration, JS::Handle options, PlainDate* result); /** @@ -213,7 +198,7 @@ bool AddDate(JSContext* cx, JS::Handle calendar, */ bool AddDate(JSContext* cx, JS::Handle calendar, JS::Handle> date, - const Duration& duration, PlainDate* result); + const DateDuration& duration, PlainDate* result); /** * DifferenceISODate ( y1, m1, d1, y2, m2, d2, largestUnit ) @@ -227,7 +212,8 @@ DateDuration DifferenceISODate(const PlainDate& start, const PlainDate& end, bool DifferenceDate(JSContext* cx, JS::Handle calendar, JS::Handle> one, JS::Handle> two, - JS::Handle options, Duration* result); + TemporalUnit largestUnit, JS::Handle options, + DateDuration* result); /** * DifferenceDate ( calendarRec, one, two, options ) @@ -235,7 +221,22 @@ bool DifferenceDate(JSContext* cx, JS::Handle calendar, bool DifferenceDate(JSContext* cx, JS::Handle calendar, JS::Handle> one, JS::Handle> two, - TemporalUnit largestUnit, Duration* result); + TemporalUnit largestUnit, DateDuration* result); + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool DifferenceDate(JSContext* cx, JS::Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, JS::Handle options, + DateDuration* result); + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool DifferenceDate(JSContext* cx, JS::Handle calendar, + const PlainDate& one, const PlainDate& two, + TemporalUnit largestUnit, DateDuration* result); /** * CompareISODate ( y1, m1, d1, y2, m2, d2 ) @@ -245,7 +246,7 @@ int32_t CompareISODate(const PlainDate& one, const PlainDate& two); /** * BalanceISODate ( year, month, day ) */ -bool BalanceISODate(JSContext* cx, int32_t year, int32_t month, int64_t day, +bool BalanceISODate(JSContext* cx, const PlainDate& date, int64_t days, PlainDate* result); /** diff --git a/js/src/builtin/temporal/PlainDateTime.cpp b/js/src/builtin/temporal/PlainDateTime.cpp index 8f137cfe43..f4b15422b3 100644 --- a/js/src/builtin/temporal/PlainDateTime.cpp +++ b/js/src/builtin/temporal/PlainDateTime.cpp @@ -178,11 +178,6 @@ static bool ISODateTimeWithinLimits(T year, T month, T day) { // components set to zero. That means the maximum value is exclusive, whereas // the minimum value is inclusive. - // FIXME: spec bug - GetUTCEpochNanoseconds when called with large |year| may - // cause MakeDay to return NaN, which makes MakeDate return NaN, which is - // unexpected in GetUTCEpochNanoseconds, step 4. - // https://github.com/tc39/proposal-temporal/issues/2315 - // Definitely in range. if (minYear < year && year < maxYear) { return true; @@ -229,7 +224,7 @@ bool js::temporal::ISODateTimeWithinLimits(double year, double month, * millisecond, microsecond, nanosecond ) */ bool js::temporal::ISODateTimeWithinLimits(const PlainDateTime& dateTime) { - auto& [date, time] = dateTime; + const auto& [date, time] = dateTime; return ::ISODateTimeWithinLimits(date.year, date.month, date.day, time.hour, time.minute, time.second, time.millisecond, time.microsecond, time.nanosecond); @@ -295,37 +290,39 @@ static PlainDateTimeObject* CreateTemporalDateTime( // Step 6. dateTime->setFixedSlot(PlainDateTimeObject::ISO_YEAR_SLOT, - Int32Value(isoYear)); + Int32Value(int32_t(isoYear))); // Step 7. dateTime->setFixedSlot(PlainDateTimeObject::ISO_MONTH_SLOT, - Int32Value(isoMonth)); + Int32Value(int32_t(isoMonth))); // Step 8. - dateTime->setFixedSlot(PlainDateTimeObject::ISO_DAY_SLOT, Int32Value(isoDay)); + dateTime->setFixedSlot(PlainDateTimeObject::ISO_DAY_SLOT, + Int32Value(int32_t(isoDay))); // Step 9. - dateTime->setFixedSlot(PlainDateTimeObject::ISO_HOUR_SLOT, Int32Value(hour)); + dateTime->setFixedSlot(PlainDateTimeObject::ISO_HOUR_SLOT, + Int32Value(int32_t(hour))); // Step 10. dateTime->setFixedSlot(PlainDateTimeObject::ISO_MINUTE_SLOT, - Int32Value(minute)); + Int32Value(int32_t(minute))); // Step 11. dateTime->setFixedSlot(PlainDateTimeObject::ISO_SECOND_SLOT, - Int32Value(second)); + Int32Value(int32_t(second))); // Step 12. dateTime->setFixedSlot(PlainDateTimeObject::ISO_MILLISECOND_SLOT, - Int32Value(millisecond)); + Int32Value(int32_t(millisecond))); // Step 13. dateTime->setFixedSlot(PlainDateTimeObject::ISO_MICROSECOND_SLOT, - Int32Value(microsecond)); + Int32Value(int32_t(microsecond))); // Step 14. dateTime->setFixedSlot(PlainDateTimeObject::ISO_NANOSECOND_SLOT, - Int32Value(nanosecond)); + Int32Value(int32_t(nanosecond))); // Step 15. dateTime->setFixedSlot(PlainDateTimeObject::CALENDAR_SLOT, @@ -342,9 +339,10 @@ static PlainDateTimeObject* CreateTemporalDateTime( PlainDateTimeObject* js::temporal::CreateTemporalDateTime( JSContext* cx, const PlainDateTime& dateTime, Handle calendar) { - auto& [date, time] = dateTime; - auto& [isoYear, isoMonth, isoDay] = date; - auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + const auto& [date, time] = dateTime; + const auto& [isoYear, isoMonth, isoDay] = date; + const auto& [hour, minute, second, millisecond, microsecond, nanosecond] = + time; // Steps 1-2. if (!ThrowIfInvalidISODateTime(cx, dateTime)) { @@ -441,7 +439,7 @@ bool js::temporal::InterpretTemporalDateTimeFields( CalendarMethod::DateFromFields)); // Step 3. - TimeRecord timeResult; + TemporalTimeLike timeResult; if (!ToTemporalTimeRecord(cx, fields, &timeResult)) { return false; } @@ -698,7 +696,7 @@ bool js::temporal::ToTemporalDateTime(JSContext* cx, Handle item, static bool ToTemporalDateTime( JSContext* cx, Handle item, MutableHandle result) { - HandleObject options = nullptr; + Handle options = nullptr; auto* obj = ::ToTemporalDateTime(cx, item, options).unwrapOrNull(); if (!obj) { @@ -734,42 +732,46 @@ static int32_t CompareISODateTime(const PlainDateTime& one, /** * AddDateTime ( year, month, day, hour, minute, second, millisecond, - * microsecond, nanosecond, calendarRec, years, months, weeks, days, hours, - * minutes, seconds, milliseconds, microseconds, nanoseconds, options ) + * microsecond, nanosecond, calendarRec, years, months, weeks, days, norm, + * options ) */ static bool AddDateTime(JSContext* cx, const PlainDateTime& dateTime, Handle calendar, - const Duration& duration, Handle options, - PlainDateTime* result) { + const NormalizedDuration& duration, + Handle options, PlainDateTime* result) { MOZ_ASSERT(IsValidDuration(duration)); // Step 1. MOZ_ASSERT(IsValidISODateTime(dateTime)); - MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); // Step 2. - PlainTime timeResult; - double daysResult; - if (!AddTime(cx, dateTime.time, duration, &timeResult, &daysResult)) { - return false; - } + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); // Step 3. - const auto& datePart = dateTime.date; + auto timeResult = AddTime(dateTime.time, duration.time); // Step 4. - Duration dateDuration = {duration.years, duration.months, duration.weeks, - daysResult}; - MOZ_ASSERT(IsValidDuration(duration)); + const auto& datePart = dateTime.date; // Step 5. + auto dateDuration = DateDuration{ + duration.date.years, + duration.date.months, + duration.date.weeks, + duration.date.days + timeResult.days, + }; + if (!ThrowIfInvalidDuration(cx, dateDuration)) { + return false; + } + + // Step 6. PlainDate addedDate; if (!AddDate(cx, calendar, datePart, dateDuration, options, &addedDate)) { return false; } - // Step 6. - *result = {addedDate, timeResult}; + // Step 7. + *result = {addedDate, timeResult.time}; return true; } @@ -782,7 +784,7 @@ static bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, Handle calendar, TemporalUnit largestUnit, Handle maybeOptions, - Duration* result) { + NormalizedDuration* result) { // Steps 1-2. MOZ_ASSERT(IsValidISODateTime(one)); MOZ_ASSERT(IsValidISODateTime(two)); @@ -795,10 +797,10 @@ static bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); // Step 4. - auto timeDifference = DifferenceTime(one.time, two.time); + auto timeDuration = DifferenceTime(one.time, two.time); // Step 5. - int32_t timeSign = DurationSign(timeDifference.toDuration()); + int32_t timeSign = NormalizedTimeDurationSign(timeDuration); // Step 6. int32_t dateSign = CompareISODate(two.date, one.date); @@ -813,20 +815,8 @@ static bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, adjustedDate.day - timeSign); // Step 8.b. - if (!BalanceTimeDuration(cx, - { - 0, - 0, - 0, - double(-timeSign), - timeDifference.hours, - timeDifference.minutes, - timeDifference.seconds, - timeDifference.milliseconds, - timeDifference.microseconds, - timeDifference.nanoseconds, - }, - largestUnit, &timeDifference)) { + if (!Add24HourDaysToNormalizedTimeDuration(cx, timeDuration, -timeSign, + &timeDuration)) { return false; } } @@ -834,49 +824,26 @@ static bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, MOZ_ASSERT(IsValidISODate(adjustedDate)); MOZ_ASSERT(ISODateTimeWithinLimits(adjustedDate)); - // TODO: Avoid allocating CreateTemporalDate. - // Step 9. - Rooted date1( - cx, CreateTemporalDate(cx, adjustedDate, calendar.receiver())); - if (!date1) { - return false; - } + const auto& date1 = adjustedDate; // Step 10. - Rooted date2( - cx, CreateTemporalDate(cx, two.date, calendar.receiver())); - if (!date2) { - return false; - } + const auto& date2 = two.date; // Step 11. auto dateLargestUnit = std::min(TemporalUnit::Day, largestUnit); - Duration dateDifference; + DateDuration dateDifference; if (maybeOptions) { - // FIXME: spec issue - this copy is no longer needed, all callers have - // already copied the user input object. - // https://github.com/tc39/proposal-temporal/issues/2525 - // Step 12. - Rooted untilOptions(cx, - SnapshotOwnProperties(cx, maybeOptions)); - if (!untilOptions) { - return false; - } + // + // The spec performs an unnecessary copy operation. As an optimization, we + // omit this copy. + auto untilOptions = maybeOptions; - // Step 13. - Rooted largestUnitValue( - cx, StringValue(TemporalUnitToString(cx, dateLargestUnit))); - if (!DefineDataProperty(cx, untilOptions, cx->names().largestUnit, - largestUnitValue)) { - return false; - } - - // Step 14. - if (!DifferenceDate(cx, calendar, date1, date2, untilOptions, - &dateDifference)) { + // Steps 13-14. + if (!DifferenceDate(cx, calendar, date1, date2, dateLargestUnit, + untilOptions, &dateDifference)) { return false; } } else { @@ -888,82 +855,32 @@ static bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, } // Step 15. - TimeDuration balanceResult; - if (!BalanceTimeDuration(cx, - { - 0, - 0, - 0, - dateDifference.days, - timeDifference.hours, - timeDifference.minutes, - timeDifference.seconds, - timeDifference.milliseconds, - timeDifference.microseconds, - timeDifference.nanoseconds, - }, - largestUnit, &balanceResult)) { - return false; - } - - // Step 16. - *result = {dateDifference.years, dateDifference.months, - dateDifference.weeks, balanceResult.days, - balanceResult.hours, balanceResult.minutes, - balanceResult.seconds, balanceResult.milliseconds, - balanceResult.microseconds, balanceResult.nanoseconds}; - MOZ_ASSERT(IsValidDuration(*result)); - return true; -} - -/** - * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, - * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) - */ -bool js::temporal::DifferenceISODateTime(JSContext* cx, - const PlainDateTime& one, - const PlainDateTime& two, - Handle calendar, - TemporalUnit largestUnit, - Duration* result) { - return ::DifferenceISODateTime(cx, one, two, calendar, largestUnit, nullptr, - result); -} - -/** - * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, - * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) - */ -bool js::temporal::DifferenceISODateTime( - JSContext* cx, const PlainDateTime& one, const PlainDateTime& two, - Handle calendar, TemporalUnit largestUnit, - Handle options, Duration* result) { - return ::DifferenceISODateTime(cx, one, two, calendar, largestUnit, options, - result); + return CreateNormalizedDurationRecord(cx, dateDifference, timeDuration, + result); } /** * RoundISODateTime ( year, month, day, hour, minute, second, millisecond, - * microsecond, nanosecond, increment, unit, roundingMode [ , dayLength ] ) + * microsecond, nanosecond, increment, unit, roundingMode ) */ -static PlainDateTime RoundISODateTime(const PlainDateTime& dateTime, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode) { +PlainDateTime js::temporal::RoundISODateTime( + const PlainDateTime& dateTime, Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode) { const auto& [date, time] = dateTime; // Step 1. MOZ_ASSERT(IsValidISODateTime(dateTime)); - MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); - // Step 2. (Not applicable in our implementation.) + // Step 2. + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); // Step 3. auto roundedTime = RoundTime(time, increment, unit, roundingMode); MOZ_ASSERT(0 <= roundedTime.days && roundedTime.days <= 1); // Step 4. - auto balanceResult = - BalanceISODate(date.year, date.month, date.day + roundedTime.days); + auto balanceResult = BalanceISODate(date.year, date.month, + date.day + int32_t(roundedTime.days)); // Step 5. return {balanceResult, roundedTime.time}; @@ -1049,7 +966,7 @@ static bool DifferenceTemporalPlainDateTime(JSContext* cx, } // Step 10. - Duration diff; + NormalizedDuration diff; if (!::DifferenceISODateTime(cx, dateTime, other, calendar, settings.largestUnit, resolvedOptions, &diff)) { return false; @@ -1060,62 +977,81 @@ static bool DifferenceTemporalPlainDateTime(JSContext* cx, settings.smallestUnit == TemporalUnit::Nanosecond && settings.roundingIncrement == Increment{1}; - // Step 12. - if (roundingGranularityIsNoop) { - if (operation == TemporalDifference::Since) { - diff = diff.negate(); + // Steps 12-13. + DateDuration balancedDate; + TimeDuration balancedTime; + if (!roundingGranularityIsNoop) { + // Step 12.a. + Rooted relativeTo( + cx, CreateTemporalDate(cx, dateTime.date(), dateTime.calendar())); + if (!relativeTo) { + return false; } - auto* obj = CreateTemporalDuration(cx, diff); - if (!obj) { + // Steps 12.b-c. + NormalizedDuration roundResult; + if (!temporal::RoundDuration(cx, diff, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + relativeTo, calendar, &roundResult)) { return false; } - args.rval().setObject(*obj); - return true; - } + // Step 12.d. + NormalizedTimeDuration withDays; + if (!Add24HourDaysToNormalizedTimeDuration( + cx, roundResult.time, roundResult.date.days, &withDays)) { + return false; + } - // Step 13. - Rooted relativeTo( - cx, CreateTemporalDate(cx, dateTime.date(), dateTime.calendar())); - if (!relativeTo) { - return false; - } + // Step 12.e. + if (!BalanceTimeDuration(cx, withDays, settings.largestUnit, + &balancedTime)) { + return false; + } - // Steps 14-15. - Duration roundResult; - if (!temporal::RoundDuration(cx, diff, settings.roundingIncrement, - settings.smallestUnit, settings.roundingMode, - relativeTo, calendar, &roundResult)) { - return false; - } + // Step 12.f. + auto toBalance = DateDuration{ + roundResult.date.years, + roundResult.date.months, + roundResult.date.weeks, + balancedTime.days, + }; + if (!temporal::BalanceDateDurationRelative( + cx, toBalance, settings.largestUnit, settings.smallestUnit, + relativeTo, calendar, &balancedDate)) { + return false; + } + } else { + // Step 13.a. + NormalizedTimeDuration withDays; + if (!Add24HourDaysToNormalizedTimeDuration(cx, diff.time, diff.date.days, + &withDays)) { + return false; + } - // Step 16. - TimeDuration result; - if (!BalanceTimeDuration(cx, roundResult, settings.largestUnit, &result)) { - return false; - } + // Step 13.b. + if (!BalanceTimeDuration(cx, withDays, settings.largestUnit, + &balancedTime)) { + return false; + } - // Step 17. - auto toBalance = Duration{ - roundResult.years, - roundResult.months, - roundResult.weeks, - result.days, - }; - DateDuration balanceResult; - if (!temporal::BalanceDateDurationRelative( - cx, toBalance, settings.largestUnit, settings.smallestUnit, - relativeTo, calendar, &balanceResult)) { - return false; + // Step 13.c. + balancedDate = { + diff.date.years, + diff.date.months, + diff.date.weeks, + balancedTime.days, + }; } + MOZ_ASSERT(IsValidDuration(balancedDate)); - // Step 18. + // Step 14. Duration duration = { - balanceResult.years, balanceResult.months, balanceResult.weeks, - balanceResult.days, result.hours, result.minutes, - result.seconds, result.milliseconds, result.microseconds, - result.nanoseconds, + double(balancedDate.years), double(balancedDate.months), + double(balancedDate.weeks), double(balancedDate.days), + double(balancedTime.hours), double(balancedTime.minutes), + double(balancedTime.seconds), double(balancedTime.milliseconds), + balancedTime.microseconds, balancedTime.nanoseconds, }; if (operation == TemporalDifference::Since) { duration = duration.negate(); @@ -1176,16 +1112,18 @@ static bool AddDurationToOrSubtractDurationFromPlainDateTime( if (operation == PlainDateTimeDuration::Subtract) { duration = duration.negate(); } + auto normalized = CreateNormalizedDurationRecord(duration); + // Step 6 PlainDateTime result; - if (!AddDateTime(cx, dateTime, calendar, duration, options, &result)) { + if (!AddDateTime(cx, dateTime, calendar, normalized, options, &result)) { return false; } - // Steps 6-7. + // Steps 7-8. MOZ_ASSERT(IsValidISODateTime(result)); - // Step 8. + // Step 9. auto* obj = CreateTemporalDateTime(cx, result, dateTime.calendar()); if (!obj) { return false; @@ -1824,13 +1762,11 @@ static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { if (!temporalDateTimeLike) { return false; } - - // Step 4. - if (!RejectTemporalLikeObject(cx, temporalDateTimeLike)) { + if (!ThrowIfTemporalLikeObject(cx, temporalDateTimeLike)) { return false; } - // Step 5. + // Step 4. Rooted resolvedOptions(cx); if (args.hasDefined(1)) { Rooted options(cx, @@ -1846,7 +1782,7 @@ static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 6. + // Step 5. Rooted calendarValue(cx, dateTime->calendar()); Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, calendarValue, @@ -1859,7 +1795,7 @@ static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 7. + // Step 6. JS::RootedVector fieldNames(cx); if (!CalendarFields(cx, calendar, {CalendarField::Day, CalendarField::Month, @@ -1868,14 +1804,14 @@ static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 8. + // Step 7. Rooted fields(cx, PrepareTemporalFields(cx, dateTime, fieldNames)); if (!fields) { return false; } - // Steps 9-14. + // Steps 8-13. struct TimeField { using FieldName = ImmutableTenuredPtr JSAtomState::*; @@ -1900,7 +1836,7 @@ static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { } } - // Step 15. + // Step 14. if (!AppendSorted(cx, fieldNames.get(), { TemporalField::Hour, @@ -1913,37 +1849,37 @@ static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 16. + // Step 15. Rooted partialDateTime( cx, PreparePartialTemporalFields(cx, temporalDateTimeLike, fieldNames)); if (!partialDateTime) { return false; } - // Step 17. + // Step 16. Rooted mergedFields( cx, CalendarMergeFields(cx, calendar, fields, partialDateTime)); if (!mergedFields) { return false; } - // Step 18. + // Step 17. fields = PrepareTemporalFields(cx, mergedFields, fieldNames); if (!fields) { return false; } - // Step 19. + // Step 18. PlainDateTime result; if (!InterpretTemporalDateTimeFields(cx, calendar, fields, resolvedOptions, &result)) { return false; } - // Steps 20-21. + // Steps 19-20. MOZ_ASSERT(IsValidISODateTime(result)); - // Step 22. + // Step 21. auto* obj = CreateTemporalDateTime(cx, result, calendar.receiver()); if (!obj) { return false; @@ -1970,7 +1906,7 @@ static bool PlainDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { auto date = ToPlainDate(temporalDateTime); Rooted calendar(cx, temporalDateTime->calendar()); - // Step 4. + // Step 3. (Inlined ToTemporalTimeOrMidnight) PlainTime time = {}; if (args.hasDefined(0)) { if (!ToTemporalTime(cx, args[0], &time)) { @@ -1978,7 +1914,7 @@ static bool PlainDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { } } - // Steps 3 and 5. + // Step 4. auto* obj = CreateTemporalDateTime(cx, {date, time}, calendar); if (!obj) { return false; diff --git a/js/src/builtin/temporal/PlainDateTime.h b/js/src/builtin/temporal/PlainDateTime.h index 3546fca903..1ae80a4508 100644 --- a/js/src/builtin/temporal/PlainDateTime.h +++ b/js/src/builtin/temporal/PlainDateTime.h @@ -8,6 +8,7 @@ #define builtin_temporal_PlainDateTime_h #include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" #include @@ -104,6 +105,8 @@ inline PlainDateTime ToPlainDateTime(const PlainDateTimeObject* dateTime) { return {ToPlainDate(dateTime), ToPlainTime(dateTime)}; } +class Increment; +enum class TemporalRoundingMode; enum class TemporalUnit; #ifdef DEBUG @@ -170,25 +173,14 @@ bool InterpretTemporalDateTimeFields(JSContext* cx, PlainDateTime* result); /** - * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, - * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) + * RoundISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond, increment, unit, roundingMode ) */ -bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, - const PlainDateTime& two, - JS::Handle calendar, - TemporalUnit largestUnit, Duration* result); +PlainDateTime RoundISODateTime(const PlainDateTime& dateTime, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode); -/** - * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, - * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) - */ -bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, - const PlainDateTime& two, - JS::Handle calendar, - TemporalUnit largestUnit, - JS::Handle options, Duration* result); - -class PlainDateTimeWithCalendar { +class MOZ_STACK_CLASS PlainDateTimeWithCalendar final { PlainDateTime dateTime_; CalendarValue calendar_; diff --git a/js/src/builtin/temporal/PlainMonthDay.cpp b/js/src/builtin/temporal/PlainMonthDay.cpp index 0896100a3f..50618a5fb6 100644 --- a/js/src/builtin/temporal/PlainMonthDay.cpp +++ b/js/src/builtin/temporal/PlainMonthDay.cpp @@ -96,16 +96,19 @@ static PlainMonthDayObject* CreateTemporalMonthDay( } // Step 5. - obj->setFixedSlot(PlainMonthDayObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + obj->setFixedSlot(PlainMonthDayObject::ISO_MONTH_SLOT, + Int32Value(int32_t(isoMonth))); // Step 6. - obj->setFixedSlot(PlainMonthDayObject::ISO_DAY_SLOT, Int32Value(isoDay)); + obj->setFixedSlot(PlainMonthDayObject::ISO_DAY_SLOT, + Int32Value(int32_t(isoDay))); // Step 7. obj->setFixedSlot(PlainMonthDayObject::CALENDAR_SLOT, calendar.toValue()); // Step 8. - obj->setFixedSlot(PlainMonthDayObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + obj->setFixedSlot(PlainMonthDayObject::ISO_YEAR_SLOT, + Int32Value(int32_t(isoYear))); // Step 9. return obj; @@ -117,7 +120,7 @@ static PlainMonthDayObject* CreateTemporalMonthDay( */ PlainMonthDayObject* js::temporal::CreateTemporalMonthDay( JSContext* cx, const PlainDate& date, Handle calendar) { - auto& [isoYear, isoMonth, isoDay] = date; + const auto& [isoYear, isoMonth, isoDay] = date; // Step 1. if (!ThrowIfInvalidISODate(cx, date)) { @@ -307,8 +310,6 @@ static Wrapped ToTemporalMonthDay( return nullptr; } - // FIXME: spec bug - missing call to CreateCalendarMethodsRecord - // Step 13. Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, calendarValue, @@ -530,13 +531,11 @@ static bool PlainMonthDay_with(JSContext* cx, const CallArgs& args) { if (!temporalMonthDayLike) { return false; } - - // Step 4. - if (!RejectTemporalLikeObject(cx, temporalMonthDayLike)) { + if (!ThrowIfTemporalLikeObject(cx, temporalMonthDayLike)) { return false; } - // Step 5. + // Step 4. Rooted resolvedOptions(cx); if (args.hasDefined(1)) { Rooted options(cx, @@ -552,7 +551,7 @@ static bool PlainMonthDay_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 6. + // Step 5. Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, calendarValue, { @@ -564,7 +563,7 @@ static bool PlainMonthDay_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 7. + // Step 6. JS::RootedVector fieldNames(cx); if (!CalendarFields(cx, calendar, {CalendarField::Day, CalendarField::Month, @@ -573,34 +572,34 @@ static bool PlainMonthDay_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 8. + // Step 7. Rooted fields(cx, PrepareTemporalFields(cx, monthDay, fieldNames)); if (!fields) { return false; } - // Step 9. + // Step 8. Rooted partialMonthDay( cx, PreparePartialTemporalFields(cx, temporalMonthDayLike, fieldNames)); if (!partialMonthDay) { return false; } - // Step 10. + // Step 9. Rooted mergedFields( cx, CalendarMergeFields(cx, calendar, fields, partialMonthDay)); if (!mergedFields) { return false; } - // Step 11. + // Step 10. fields = PrepareTemporalFields(cx, mergedFields, fieldNames); if (!fields) { return false; } - // Step 12. + // Step 11. auto obj = js::temporal::CalendarMonthDayFromFields(cx, calendar, fields, resolvedOptions); if (!obj) { @@ -836,20 +835,7 @@ static bool PlainMonthDay_toPlainDate(JSContext* cx, const CallArgs& args) { } // Step 12. - Rooted options(cx, NewPlainObjectWithProto(cx, nullptr)); - if (!options) { - return false; - } - - // Step 13. - Rooted overflow(cx, StringValue(cx->names().constrain)); - if (!DefineDataProperty(cx, options, cx->names().overflow, overflow)) { - return false; - } - - // Step 14. - auto obj = js::temporal::CalendarDateFromFields( - cx, calendar, mergedFromConcatenatedFields, options); + auto obj = CalendarDateFromFields(cx, calendar, mergedFromConcatenatedFields); if (!obj) { return false; } diff --git a/js/src/builtin/temporal/PlainTime.cpp b/js/src/builtin/temporal/PlainTime.cpp index bf35b9d93e..c928b06d46 100644 --- a/js/src/builtin/temporal/PlainTime.cpp +++ b/js/src/builtin/temporal/PlainTime.cpp @@ -7,7 +7,6 @@ #include "builtin/temporal/PlainTime.h" #include "mozilla/Assertions.h" -#include "mozilla/CheckedInt.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" @@ -46,7 +45,6 @@ #include "js/PropertySpec.h" #include "js/RootingAPI.h" #include "js/Value.h" -#include "vm/BigIntType.h" #include "vm/BytecodeUtil.h" #include "vm/GlobalObject.h" #include "vm/JSAtomState.h" @@ -121,7 +119,8 @@ static bool IsValidTime(T hour, T minute, T second, T millisecond, * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) */ bool js::temporal::IsValidTime(const PlainTime& time) { - auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + const auto& [hour, minute, second, millisecond, microsecond, nanosecond] = + time; return ::IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond); } @@ -217,7 +216,8 @@ static bool ThrowIfInvalidTime(JSContext* cx, T hour, T minute, T second, * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) */ bool js::temporal::ThrowIfInvalidTime(JSContext* cx, const PlainTime& time) { - auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + const auto& [hour, minute, second, millisecond, microsecond, nanosecond] = + time; return ::ThrowIfInvalidTime(cx, hour, minute, second, millisecond, microsecond, nanosecond); } @@ -261,9 +261,10 @@ static PlainTime ConstrainTime(double hour, double minute, double second, * RegulateTime ( hour, minute, second, millisecond, microsecond, nanosecond, * overflow ) */ -bool js::temporal::RegulateTime(JSContext* cx, const TimeRecord& time, +bool js::temporal::RegulateTime(JSContext* cx, const TemporalTimeLike& time, TemporalOverflow overflow, PlainTime* result) { - auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + const auto& [hour, minute, second, millisecond, microsecond, nanosecond] = + time; // Step 1. MOZ_ASSERT(IsInteger(hour)); @@ -334,25 +335,28 @@ static PlainTimeObject* CreateTemporalTime(JSContext* cx, const CallArgs& args, } // Step 4. - object->setFixedSlot(PlainTimeObject::ISO_HOUR_SLOT, Int32Value(hour)); + object->setFixedSlot(PlainTimeObject::ISO_HOUR_SLOT, + Int32Value(int32_t(hour))); // Step 5. - object->setFixedSlot(PlainTimeObject::ISO_MINUTE_SLOT, Int32Value(minute)); + object->setFixedSlot(PlainTimeObject::ISO_MINUTE_SLOT, + Int32Value(int32_t(minute))); // Step 6. - object->setFixedSlot(PlainTimeObject::ISO_SECOND_SLOT, Int32Value(second)); + object->setFixedSlot(PlainTimeObject::ISO_SECOND_SLOT, + Int32Value(int32_t(second))); // Step 7. object->setFixedSlot(PlainTimeObject::ISO_MILLISECOND_SLOT, - Int32Value(millisecond)); + Int32Value(int32_t(millisecond))); // Step 8. object->setFixedSlot(PlainTimeObject::ISO_MICROSECOND_SLOT, - Int32Value(microsecond)); + Int32Value(int32_t(microsecond))); // Step 9. object->setFixedSlot(PlainTimeObject::ISO_NANOSECOND_SLOT, - Int32Value(nanosecond)); + Int32Value(int32_t(nanosecond))); // Step 10. return object; @@ -364,7 +368,8 @@ static PlainTimeObject* CreateTemporalTime(JSContext* cx, const CallArgs& args, */ PlainTimeObject* js::temporal::CreateTemporalTime(JSContext* cx, const PlainTime& time) { - auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + const auto& [hour, minute, second, millisecond, microsecond, nanosecond] = + time; // Step 1. if (!ThrowIfInvalidTime(cx, time)) { @@ -402,63 +407,6 @@ PlainTimeObject* js::temporal::CreateTemporalTime(JSContext* cx, return object; } -/** - * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) - */ -static TimeDuration CreateTimeDurationRecord(double days, int32_t hours, - int32_t minutes, int32_t seconds, - int32_t milliseconds, - int32_t microseconds, - int32_t nanoseconds) { - // Step 1. - MOZ_ASSERT(IsValidDuration({0, 0, 0, days, double(hours), double(minutes), - double(seconds), double(microseconds), - double(nanoseconds)})); - - // Step 2. - return { - days, - double(hours), - double(minutes), - double(seconds), - double(milliseconds), - double(microseconds), - double(nanoseconds), - }; -} - -/** - * DurationSign ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds ) - */ -static int32_t DurationSign(int32_t hours, int32_t minutes, int32_t seconds, - int32_t milliseconds, int32_t microseconds, - int32_t nanoseconds) { - // Step 1. (Loop unrolled) - if (hours) { - return hours > 0 ? 1 : -1; - } - if (minutes) { - return minutes > 0 ? 1 : -1; - } - if (seconds) { - return seconds > 0 ? 1 : -1; - } - if (milliseconds) { - return milliseconds > 0 ? 1 : -1; - } - if (microseconds) { - return microseconds > 0 ? 1 : -1; - } - if (nanoseconds) { - return nanoseconds > 0 ? 1 : -1; - } - - // Step 2. - return 0; -} - /** * BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ) */ @@ -531,7 +479,7 @@ static BalancedTime BalanceTime(int32_t hour, int32_t minute, int32_t second, BalancedTime js::temporal::BalanceTime(const PlainTime& time, int64_t nanoseconds) { MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(std::abs(nanoseconds) <= 2 * ToNanoseconds(TemporalUnit::Day)); + MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day)); return ::BalanceTime(time.hour, time.minute, time.second, time.millisecond, time.microsecond, @@ -541,8 +489,8 @@ BalancedTime js::temporal::BalanceTime(const PlainTime& time, /** * DifferenceTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, ns2 ) */ -TimeDuration js::temporal::DifferenceTime(const PlainTime& time1, - const PlainTime& time2) { +NormalizedTimeDuration js::temporal::DifferenceTime(const PlainTime& time1, + const PlainTime& time2) { MOZ_ASSERT(IsValidTime(time1)); MOZ_ASSERT(IsValidTime(time2)); @@ -565,22 +513,15 @@ TimeDuration js::temporal::DifferenceTime(const PlainTime& time1, int32_t nanoseconds = time2.nanosecond - time1.nanosecond; // Step 7. - int32_t sign = ::DurationSign(hours, minutes, seconds, milliseconds, - microseconds, nanoseconds); + auto result = NormalizeTimeDuration(hours, minutes, seconds, milliseconds, + microseconds, nanoseconds); // Step 8. - auto balanced = ::BalanceTime(hours * sign, minutes * sign, seconds * sign, - milliseconds * sign, microseconds * sign, - nanoseconds * sign); + MOZ_ASSERT(result.abs().toNanoseconds() < + Int128{ToNanoseconds(TemporalUnit::Day)}); // Step 9. - MOZ_ASSERT(balanced.days == 0); - - // Step 10. - return CreateTimeDurationRecord( - 0, balanced.time.hour * sign, balanced.time.minute * sign, - balanced.time.second * sign, balanced.time.millisecond * sign, - balanced.time.microsecond * sign, balanced.time.nanosecond * sign); + return result; } /** @@ -628,7 +569,7 @@ static bool ToTemporalTime(JSContext* cx, Handle item, } // Step 3.d. - TimeRecord timeResult; + TemporalTimeLike timeResult; if (!ToTemporalTimeRecord(cx, itemObj, &timeResult)) { return false; } @@ -683,218 +624,6 @@ bool js::temporal::ToTemporalTime(JSContext* cx, Handle item, return ToTemporalTime(cx, item, TemporalOverflow::Constrain, result); } -/** - * TotalDurationNanoseconds ( hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) - */ -static int64_t TotalDurationNanoseconds(const Duration& duration) { - // This function is only called from BalanceTime. The difference between two - // plain times can't exceed the number of nanoseconds in a day. - MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(std::abs(duration.hours) <= 24); - MOZ_ASSERT(std::abs(duration.minutes) <= 60); - MOZ_ASSERT(std::abs(duration.seconds) <= 60); - MOZ_ASSERT(std::abs(duration.milliseconds) <= 1000); - MOZ_ASSERT(std::abs(duration.microseconds) <= 1000); - MOZ_ASSERT(std::abs(duration.nanoseconds) <= 1000); - - // Step 1. - auto minutes = int64_t(duration.minutes) + int64_t(duration.hours) * 60; - - // Step 2. - auto seconds = int64_t(duration.seconds) + minutes * 60; - - // Step 3. - auto milliseconds = int64_t(duration.milliseconds) + seconds * 1000; - - // Step 4. - auto microseconds = int64_t(duration.microseconds) + milliseconds * 1000; - - // Steps 5. - return int64_t(duration.nanoseconds) + microseconds * 1000; -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit [ , relativeTo ] ) - */ -static Duration BalanceTimeDuration(const Duration& duration, - TemporalUnit largestUnit) { - MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(largestUnit > TemporalUnit::Day); - - // We only handle time components here. - MOZ_ASSERT(duration.years == 0); - MOZ_ASSERT(duration.months == 0); - MOZ_ASSERT(duration.weeks == 0); - MOZ_ASSERT(duration.days == 0); - - // Step 1. (Not applicable) - - // Step 2. - int64_t nanoseconds = TotalDurationNanoseconds(duration); - MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day)); - - // Steps 3-4. (Not applicable) - - // Step 5. - int64_t hours = 0; - int64_t minutes = 0; - int64_t seconds = 0; - int64_t milliseconds = 0; - int64_t microseconds = 0; - - // Step 6. - int32_t sign = nanoseconds < 0 ? -1 : +1; - - // Step 7. - nanoseconds = std::abs(nanoseconds); - - // Steps 8-13. - switch (largestUnit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Month: - case TemporalUnit::Week: - case TemporalUnit::Day: - MOZ_CRASH("Unexpected temporal unit"); - - case TemporalUnit::Hour: { - // Step 8. - - // Step 8.a. - microseconds = nanoseconds / 1000; - - // Step 8.b. - nanoseconds = nanoseconds % 1000; - - // Step 8.c. - milliseconds = microseconds / 1000; - - // Step 8.d. - microseconds = microseconds % 1000; - - // Step 8.e. - seconds = milliseconds / 1000; - - // Step 8.f. - milliseconds = milliseconds % 1000; - - // Step 8.g. - minutes = seconds / 60; - - // Step 8.h. - seconds = seconds % 60; - - // Step 8.i. - hours = minutes / 60; - - // Step 8.j. - minutes = minutes % 60; - - break; - } - case TemporalUnit::Minute: { - // Step 9. - - // Step 9.a. - microseconds = nanoseconds / 1000; - - // Step 9.b. - nanoseconds = nanoseconds % 1000; - - // Step 9.c. - milliseconds = microseconds / 1000; - - // Step 9.d. - microseconds = microseconds % 1000; - - // Step 9.e. - seconds = milliseconds / 1000; - - // Step 9.f. - milliseconds = milliseconds % 1000; - - // Step 9.g. - minutes = seconds / 60; - - // Step 9.h. - seconds = seconds % 60; - - break; - } - case TemporalUnit::Second: { - // Step 10. - - // Step 10.a. - microseconds = nanoseconds / 1000; - - // Step 10.b. - nanoseconds = nanoseconds % 1000; - - // Step 10.c. - milliseconds = microseconds / 1000; - - // Step 10.d. - microseconds = microseconds % 1000; - - // Step 10.e. - seconds = milliseconds / 1000; - - // Step 10.f. - milliseconds = milliseconds % 1000; - - break; - } - case TemporalUnit::Millisecond: { - // Step 11. - - // Step 11.a. - microseconds = nanoseconds / 1000; - - // Step 11.b. - nanoseconds = nanoseconds % 1000; - - // Step 11.c. - milliseconds = microseconds / 1000; - - // Step 11.d. - microseconds = microseconds % 1000; - - break; - } - case TemporalUnit::Microsecond: { - // Step 12. - - // Step 12.a. - microseconds = nanoseconds / 1000; - - // Step 12.b. - nanoseconds = nanoseconds % 1000; - - break; - } - case TemporalUnit::Nanosecond: { - // Step 13. - break; - } - } - - // Step 14. - return { - 0, - 0, - 0, - 0, - double(hours * sign), - double(minutes * sign), - double(seconds * sign), - double(milliseconds * sign), - double(microseconds * sign), - double(nanoseconds * sign), - }; -} - /** * CompareTemporalTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, * ns2 ) @@ -940,7 +669,7 @@ int32_t js::temporal::CompareTemporalTime(const PlainTime& one, */ static bool ToTemporalTimeRecord(JSContext* cx, Handle temporalTimeLike, - TimeRecord* result) { + TemporalTimeLike* result) { // Steps 1 and 3-4. (Not applicable in our implementation.) // Step 2. (Inlined call to PrepareTemporalFields.) @@ -1012,7 +741,7 @@ static bool ToTemporalTimeRecord(JSContext* cx, */ bool js::temporal::ToTemporalTimeRecord(JSContext* cx, Handle temporalTimeLike, - TimeRecord* result) { + TemporalTimeLike* result) { // Step 3.a. (Set all fields to zero.) *result = {}; @@ -1020,59 +749,6 @@ bool js::temporal::ToTemporalTimeRecord(JSContext* cx, return ::ToTemporalTimeRecord(cx, temporalTimeLike, result); } -/** - * RoundNumberToIncrement ( x, increment, roundingMode ) - */ -static int64_t RoundNumberToIncrement(int64_t x, TemporalUnit unit, - Increment increment, - TemporalRoundingMode roundingMode) { - MOZ_ASSERT(x >= 0); - MOZ_ASSERT(x < ToNanoseconds(TemporalUnit::Day)); - - MOZ_ASSERT(unit >= TemporalUnit::Day); - MOZ_ASSERT_IF(unit == TemporalUnit::Day, increment == Increment{1}); - MOZ_ASSERT_IF(unit > TemporalUnit::Day, - increment <= MaximumTemporalDurationRoundingIncrement(unit)); - - int64_t divisor = ToNanoseconds(unit) * increment.value(); - MOZ_ASSERT(divisor > 0); - MOZ_ASSERT(divisor <= ToNanoseconds(TemporalUnit::Day)); - - // Division by one has no remainder. - if (divisor == 1) { - MOZ_ASSERT(increment == Increment{1}); - return x; - } - - // Steps 1-8. - int64_t rounded = Divide(x, divisor, roundingMode); - - // Step 9. - mozilla::CheckedInt64 result = rounded; - result *= increment.value(); - - MOZ_ASSERT(result.isValid(), "can't overflow when inputs are all in range"); - - return result.value(); -} - -/** - * RoundNumberToIncrement ( x, increment, roundingMode ) - */ -static int64_t RoundNumberToIncrement(int64_t x, int64_t divisor, - Increment increment, - TemporalRoundingMode roundingMode) { - MOZ_ASSERT(x >= 0); - MOZ_ASSERT(x < ToNanoseconds(TemporalUnit::Day)); - MOZ_ASSERT(divisor > 0); - MOZ_ASSERT(increment == Increment{1}, "Rounding increment for 'day' is 1"); - - // Steps 1-2. (Not applicable in our implementation) - - // Steps 3-8. - return Divide(x, divisor, roundingMode); -} - static int64_t TimeToNanos(const PlainTime& time) { // No overflow possible because the input is a valid time. MOZ_ASSERT(IsValidTime(time)); @@ -1090,7 +766,7 @@ static int64_t TimeToNanos(const PlainTime& time) { /** * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, - * increment, unit, roundingMode [ , dayLengthNs ] ) + * increment, unit, roundingMode ) */ RoundedTime js::temporal::RoundTime(const PlainTime& time, Increment increment, TemporalUnit unit, @@ -1163,11 +839,15 @@ RoundedTime js::temporal::RoundTime(const PlainTime& time, Increment increment, } // Step 9. - int64_t r = ::RoundNumberToIncrement(TimeToNanos(quantity), unit, increment, - roundingMode); - MOZ_ASSERT(r == int64_t(int32_t(r)), - "no overflow possible due to limited range of arguments"); - *result = r; + int64_t nanos = TimeToNanos(quantity); + MOZ_ASSERT(0 <= nanos && nanos < ToNanoseconds(TemporalUnit::Day)); + + auto r = RoundNumberToIncrement(nanos, ToNanoseconds(unit), increment, + roundingMode); + MOZ_ASSERT(r == Int128{int32_t(r)}, + "can't overflow when inputs are all in range"); + + *result = int32_t(r); // Step 10. if (unit == TemporalUnit::Day) { @@ -1181,471 +861,31 @@ RoundedTime js::temporal::RoundTime(const PlainTime& time, Increment increment, } /** - * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, - * increment, unit, roundingMode [ , dayLengthNs ] ) + * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, norm ) */ -RoundedTime js::temporal::RoundTime(const PlainTime& time, Increment increment, - TemporalUnit unit, - TemporalRoundingMode roundingMode, - const InstantSpan& dayLengthNs) { +AddedTime js::temporal::AddTime(const PlainTime& time, + const NormalizedTimeDuration& duration) { MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); - MOZ_ASSERT(dayLengthNs > (InstantSpan{})); + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); - if (unit != TemporalUnit::Day) { - return RoundTime(time, increment, unit, roundingMode); + auto [seconds, nanoseconds] = duration; + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= 1'000'000'000; } - - // Step 1. (Not applicable) - - // Step 2. - int64_t quantity = TimeToNanos(time); - MOZ_ASSERT(quantity < ToNanoseconds(TemporalUnit::Day)); - - // Steps 3-8. (Not applicable) - - // Step 9. - int64_t divisor; - if (auto checkedDiv = dayLengthNs.toNanoseconds(); checkedDiv.isValid()) { - divisor = checkedDiv.value(); - } else { - // When the divisor is too large, the expression `quantity / divisor` is a - // value near zero. Substitute |divisor| with an equivalent expression. - // Choose |86'400'000'000'000| which will give a similar result because - // |quantity| is guaranteed to be lower than |86'400'000'000'000|. - divisor = ToNanoseconds(TemporalUnit::Day); - } - MOZ_ASSERT(divisor > 0); - - int64_t result = - ::RoundNumberToIncrement(quantity, divisor, increment, roundingMode); - - // Step 10. - return {result, {0, 0, 0, 0, 0, 0}}; -} - -/** - * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, - * minutes, seconds, milliseconds, microseconds, nanoseconds ) - */ -static PlainTime AddTime(const PlainTime& time, const Duration& duration) { - MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(IsValidDuration(duration)); - - // Balance the duration so we don't have to worry about imprecise Number - // computations below. - - // Use either int64_t or int32_t below. Assert the total combined amount of - // the units can be expressed in either int64_t or int32_t. - static_assert(1 * UnitsPerDay(TemporalUnit::Nanosecond) > INT32_MAX, - "total combined nanoseconds per day"); - static_assert(2 * UnitsPerDay(TemporalUnit::Microsecond) > INT32_MAX, - "total combined microseconds per day"); - static_assert(3 * UnitsPerDay(TemporalUnit::Millisecond) <= INT32_MAX, - "total combined milliseconds per day"); - static_assert(4 * UnitsPerDay(TemporalUnit::Second) <= INT32_MAX, - "total combined seconds per day"); - static_assert(5 * UnitsPerDay(TemporalUnit::Minute) <= INT32_MAX, - "total combined minutes per day"); - static_assert(6 * UnitsPerDay(TemporalUnit::Hour) <= INT32_MAX, - "total combined hours per day"); - - // We ignore the days overflow in this function, therefore it's possible - // to restrict each unit to units-per-day. - int64_t nanoseconds = int64_t( - std::fmod(duration.nanoseconds, UnitsPerDay(TemporalUnit::Nanosecond))); - int64_t microseconds = int64_t( - std::fmod(duration.microseconds, UnitsPerDay(TemporalUnit::Microsecond))); - int32_t milliseconds = int32_t( - std::fmod(duration.milliseconds, UnitsPerDay(TemporalUnit::Millisecond))); - int32_t seconds = - int32_t(std::fmod(duration.seconds, UnitsPerDay(TemporalUnit::Second))); - int32_t minutes = - int32_t(std::fmod(duration.minutes, UnitsPerDay(TemporalUnit::Minute))); - int32_t hours = - int32_t(std::fmod(duration.hours, UnitsPerDay(TemporalUnit::Hour))); - - // Each unit is now less than the units-per-day. - MOZ_ASSERT(std::abs(nanoseconds) < UnitsPerDay(TemporalUnit::Nanosecond)); - MOZ_ASSERT(std::abs(microseconds) < UnitsPerDay(TemporalUnit::Microsecond)); - MOZ_ASSERT(std::abs(milliseconds) < UnitsPerDay(TemporalUnit::Millisecond)); - MOZ_ASSERT(std::abs(seconds) < UnitsPerDay(TemporalUnit::Second)); - MOZ_ASSERT(std::abs(minutes) < UnitsPerDay(TemporalUnit::Minute)); - MOZ_ASSERT(std::abs(hours) < UnitsPerDay(TemporalUnit::Hour)); - - microseconds += nanoseconds / 1000; - nanoseconds %= 1000; - MOZ_ASSERT(microseconds < 2 * UnitsPerDay(TemporalUnit::Microsecond)); - - milliseconds += microseconds / 1000; - microseconds %= 1000; - MOZ_ASSERT(milliseconds < 3 * UnitsPerDay(TemporalUnit::Millisecond)); - - seconds += milliseconds / 1000; - milliseconds %= 1000; - MOZ_ASSERT(seconds < 4 * UnitsPerDay(TemporalUnit::Second)); - - minutes += seconds / 60; - seconds %= 60; - MOZ_ASSERT(minutes < 5 * UnitsPerDay(TemporalUnit::Minute)); - - hours += minutes / 60; - minutes %= 60; - MOZ_ASSERT(hours < 6 * UnitsPerDay(TemporalUnit::Hour)); - - hours %= 24; - - MOZ_ASSERT(std::abs(hours) <= 23); - MOZ_ASSERT(std::abs(minutes) <= 59); - MOZ_ASSERT(std::abs(seconds) <= 59); - MOZ_ASSERT(std::abs(milliseconds) <= 999); - MOZ_ASSERT(std::abs(microseconds) <= 999); - MOZ_ASSERT(std::abs(nanoseconds) <= 999); + MOZ_ASSERT(std::abs(nanoseconds) <= 999'999'999); // Step 1. - int32_t hour = time.hour + hours; + int64_t second = time.second + seconds; // Step 2. - int32_t minute = time.minute + minutes; + int32_t nanosecond = time.nanosecond + nanoseconds; // Step 3. - int32_t second = time.second + seconds; - - // Step 4. - int32_t millisecond = time.millisecond + milliseconds; - - // Step 5. - int32_t microsecond = time.microsecond + int32_t(microseconds); - - // Step 6. - int32_t nanosecond = time.nanosecond + int32_t(nanoseconds); - - // Step 7. auto balanced = - ::BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond); - return balanced.time; -} - -static BigInt* FloorDiv(JSContext* cx, Handle dividend, - int32_t divisor) { - 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; - } - if (remainder->isNegative()) { - return BigInt::dec(cx, quotient); - } - return quotient; -} - -static bool AddTimeDaysSlow(JSContext* cx, const PlainTime& time, - const Duration& duration, double* result) { - MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(IsValidDuration(duration)); - - Rooted days(cx, BigInt::createFromDouble(cx, duration.days)); - if (!days) { - return false; - } - - Rooted hours(cx, BigInt::createFromDouble(cx, duration.hours)); - if (!hours) { - return false; - } - - Rooted minutes(cx, BigInt::createFromDouble(cx, duration.minutes)); - if (!minutes) { - return false; - } - - Rooted seconds(cx, BigInt::createFromDouble(cx, duration.seconds)); - if (!seconds) { - return false; - } - - Rooted milliseconds( - cx, BigInt::createFromDouble(cx, duration.milliseconds)); - if (!milliseconds) { - return false; - } - - Rooted microseconds( - cx, BigInt::createFromDouble(cx, duration.microseconds)); - if (!microseconds) { - return false; - } - - Rooted nanoseconds( - cx, BigInt::createFromDouble(cx, duration.nanoseconds)); - if (!nanoseconds) { - return false; - } - - auto addWithInt32 = [cx](Handle left, int32_t right) -> BigInt* { - Rooted rightBigInt(cx, BigInt::createFromInt64(cx, right)); - if (!rightBigInt) { - return nullptr; - } - return BigInt::add(cx, left, rightBigInt); - }; - - // Step 1. - Rooted hour(cx, addWithInt32(hours, time.hour)); - if (!hour) { - return false; - } - - // Step 2. - Rooted minute(cx, addWithInt32(minutes, time.minute)); - if (!minute) { - return false; - } - - // Step 3. - Rooted second(cx, addWithInt32(seconds, time.second)); - if (!second) { - return false; - } - - // Step 4. - Rooted millisecond(cx, addWithInt32(milliseconds, time.millisecond)); - if (!millisecond) { - return false; - } - - // Step 5. - Rooted microsecond(cx, addWithInt32(microseconds, time.microsecond)); - if (!microsecond) { - return false; - } - - // Step 6. - Rooted nanosecond(cx, addWithInt32(nanoseconds, time.nanosecond)); - if (!nanosecond) { - return false; - } - - // Step 7. (Inlined BalanceTime) - - auto addFloorDiv = [cx](Handle left, Handle right, - int32_t divisor) -> BigInt* { - Rooted quotient(cx, FloorDiv(cx, right, divisor)); - if (!quotient) { - return nullptr; - } - return BigInt::add(cx, left, quotient); - }; - - // BalanceTime, steps 1-2. - microsecond = addFloorDiv(microsecond, nanosecond, 1000); - if (!microsecond) { - return false; - } - - // BalanceTime, steps 3-4. - millisecond = addFloorDiv(millisecond, microsecond, 1000); - if (!millisecond) { - return false; - } - - // BalanceTime, steps 5-6. - second = addFloorDiv(second, millisecond, 1000); - if (!second) { - return false; - } - - // BalanceTime, steps 7-8. - minute = addFloorDiv(minute, second, 60); - if (!minute) { - return false; - } - - // BalanceTime, steps 9-10. - hour = addFloorDiv(hour, minute, 60); - if (!hour) { - return false; - } - - // BalanceTime, steps 11-13. - days = addFloorDiv(days, hour, 24); - if (!days) { - return false; - } - - // The days number is used as the input for a duration. Throw if the BigInt - // when converted to a Number can't be represented in a duration. - double daysNumber = BigInt::numberValue(days); - if (!ThrowIfInvalidDuration(cx, {0, 0, 0, daysNumber})) { - return false; - } - MOZ_ASSERT(IsInteger(daysNumber)); - - *result = daysNumber; - return true; -} - -static mozilla::Maybe AddTimeDays(const PlainTime& time, - const Duration& duration) { - MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(IsValidDuration(duration)); - - int64_t days; - if (!mozilla::NumberEqualsInt64(duration.days, &days)) { - return mozilla::Nothing(); - } - - int64_t hours; - if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { - return mozilla::Nothing(); - } - - int64_t minutes; - if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { - return mozilla::Nothing(); - } - - int64_t seconds; - if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { - return mozilla::Nothing(); - } - - int64_t milliseconds; - if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { - return mozilla::Nothing(); - } - - int64_t microseconds; - if (!mozilla::NumberEqualsInt64(duration.microseconds, µseconds)) { - return mozilla::Nothing(); - } - - int64_t nanoseconds; - if (!mozilla::NumberEqualsInt64(duration.nanoseconds, &nanoseconds)) { - return mozilla::Nothing(); - } - - // Step 1. - auto hour = mozilla::CheckedInt64(time.hour) + hours; - if (!hour.isValid()) { - return mozilla::Nothing(); - } - - // Step 2. - auto minute = mozilla::CheckedInt64(time.minute) + minutes; - if (!minute.isValid()) { - return mozilla::Nothing(); - } - - // Step 3. - auto second = mozilla::CheckedInt64(time.second) + seconds; - if (!second.isValid()) { - return mozilla::Nothing(); - } - - // Step 4. - auto millisecond = mozilla::CheckedInt64(time.millisecond) + milliseconds; - if (!millisecond.isValid()) { - return mozilla::Nothing(); - } - - // Step 5. - auto microsecond = mozilla::CheckedInt64(time.microsecond) + microseconds; - if (!microsecond.isValid()) { - return mozilla::Nothing(); - } - - // Step 6. - auto nanosecond = mozilla::CheckedInt64(time.nanosecond) + nanoseconds; - if (!nanosecond.isValid()) { - return mozilla::Nothing(); - } - - // Step 7. (Inlined BalanceTime) - - // BalanceTime, steps 1-2. - microsecond += FloorDiv(nanosecond.value(), 1000); - if (!microsecond.isValid()) { - return mozilla::Nothing(); - } - - // BalanceTime, steps 3-4. - millisecond += FloorDiv(microsecond.value(), 1000); - if (!millisecond.isValid()) { - return mozilla::Nothing(); - } - - // BalanceTime, steps 5-6. - second += FloorDiv(millisecond.value(), 1000); - if (!second.isValid()) { - return mozilla::Nothing(); - } - - // BalanceTime, steps 7-8. - minute += FloorDiv(second.value(), 60); - if (!minute.isValid()) { - return mozilla::Nothing(); - } - - // BalanceTime, steps 9-10. - hour += FloorDiv(minute.value(), 60); - if (!hour.isValid()) { - return mozilla::Nothing(); - } - - // BalanceTime, steps 11-13. - auto result = mozilla::CheckedInt64(days) + FloorDiv(hour.value(), 24); - if (!result.isValid()) { - return mozilla::Nothing(); - } - return mozilla::Some(result.value()); -} - -static bool AddTimeDays(JSContext* cx, const PlainTime& time, - const Duration& duration, double* result) { - // Fast-path when we can perform the whole computation with int64 values. - if (auto days = AddTimeDays(time, duration)) { - *result = *days; - return true; - } - return AddTimeDaysSlow(cx, time, duration, result); -} - -/** - * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, - * minutes, seconds, milliseconds, microseconds, nanoseconds ) - */ -bool js::temporal::AddTime(JSContext* cx, const PlainTime& time, - const Duration& duration, PlainTime* result, - double* daysResult) { - MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(IsValidDuration(duration)); - - // Steps 1-7. - auto balanced = ::AddTime(time, duration); - - // Compute |days| separately to ensure no loss of precision occurs. - // - // The single caller of this |AddTime| function also needs to compute the - // addition of |duration.days| and the balanced days. Perform this addition - // here, so we don't need to pass around BigInt values for exact mathematical - // results. - double days; - if (!AddTimeDays(cx, time, duration, &days)) { - return false; - } - MOZ_ASSERT(IsInteger(days)); - - *result = balanced; - *daysResult = days; - return true; + ::BalanceTime(time.hour, time.minute, second, time.millisecond, + time.microsecond, nanosecond); + return {balanced.days, balanced.time}; } /** @@ -1699,30 +939,28 @@ static bool DifferenceTemporalPlainTime(JSContext* cx, // Step 5. auto diff = DifferenceTime(temporalTime, other); - MOZ_ASSERT(diff.days == 0); // Step 6. - auto roundedDuration = diff.toDuration(); if (settings.smallestUnit != TemporalUnit::Nanosecond || settings.roundingIncrement != Increment{1}) { // Steps 6.a-b. - if (!RoundDuration(cx, roundedDuration.time(), settings.roundingIncrement, - settings.smallestUnit, settings.roundingMode, - &roundedDuration)) { - return false; - } + diff = RoundDuration(diff, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode); } // Step 7. - auto balancedDuration = - BalanceTimeDuration(roundedDuration, settings.largestUnit); + TimeDuration balancedDuration; + if (!BalanceTimeDuration(cx, diff, settings.largestUnit, &balancedDuration)) { + return false; + } // Step 8. + auto duration = balancedDuration.toDuration(); if (operation == TemporalDifference::Since) { - balancedDuration = balancedDuration.negate(); + duration = duration.negate(); } - auto* result = CreateTemporalDuration(cx, balancedDuration); + auto* result = CreateTemporalDuration(cx, duration); if (!result) { return false; } @@ -1754,13 +992,14 @@ static bool AddDurationToOrSubtractDurationFromPlainTime( if (operation == PlainTimeDuration::Subtract) { duration = duration.negate(); } - auto result = AddTime(time, duration); + auto timeDuration = NormalizeTimeDuration(duration); // Step 4. - MOZ_ASSERT(IsValidTime(result)); + auto result = AddTime(time, timeDuration); + MOZ_ASSERT(IsValidTime(result.time)); // Step 5. - auto* obj = CreateTemporalTime(cx, result); + auto* obj = CreateTemporalTime(cx, result.time); if (!obj) { return false; } @@ -1864,7 +1103,7 @@ static bool PlainTime_from(JSContext* cx, unsigned argc, Value* vp) { } // Steps 4-5. - auto result = ToTemporalTime(cx, args.get(0), overflow); + auto* result = ToTemporalTime(cx, args.get(0), overflow); if (!result) { return false; } @@ -2059,29 +1298,27 @@ static bool PlainTime_with(JSContext* cx, const CallArgs& args) { if (!temporalTimeLike) { return false; } - - // Step 4. - if (!RejectTemporalLikeObject(cx, temporalTimeLike)) { + if (!ThrowIfTemporalLikeObject(cx, temporalTimeLike)) { return false; } auto overflow = TemporalOverflow::Constrain; if (args.hasDefined(1)) { - // Step 5. + // Step 4. Rooted options(cx, RequireObjectArg(cx, "options", "with", args[1])); if (!options) { return false; } - // Step 6. + // Step 5. if (!ToTemporalOverflow(cx, options, &overflow)) { return false; } } - // Steps 7-19. - TimeRecord partialTime = { + // Steps 6-18. + TemporalTimeLike partialTime = { double(time.hour), double(time.minute), double(time.second), double(time.millisecond), double(time.microsecond), double(time.nanosecond), @@ -2090,13 +1327,13 @@ static bool PlainTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 20. + // Step 19. PlainTime result; if (!RegulateTime(cx, partialTime, overflow, &result)) { return false; } - // Step 21. + // Step 20. auto* obj = CreateTemporalTime(cx, result); if (!obj) { return false; diff --git a/js/src/builtin/temporal/PlainTime.h b/js/src/builtin/temporal/PlainTime.h index b6da469913..0614430e46 100644 --- a/js/src/builtin/temporal/PlainTime.h +++ b/js/src/builtin/temporal/PlainTime.h @@ -112,19 +112,24 @@ PlainTimeObject* CreateTemporalTime(JSContext* cx, const PlainTime& time); bool ToTemporalTime(JSContext* cx, JS::Handle item, PlainTime* result); +struct AddedTime { + int32_t days = 0; + PlainTime time; +}; + /** - * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, - * minutes, seconds, milliseconds, microseconds, nanoseconds ) + * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, norm ) */ -bool AddTime(JSContext* cx, const PlainTime& time, const Duration& duration, - PlainTime* result, double* daysResult); +AddedTime AddTime(const PlainTime& time, + const NormalizedTimeDuration& duration); /** * DifferenceTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, ns2 ) */ -TimeDuration DifferenceTime(const PlainTime& time1, const PlainTime& time2); +NormalizedTimeDuration DifferenceTime(const PlainTime& time1, + const PlainTime& time2); -struct TimeRecord final { +struct TemporalTimeLike final { double hour = 0; double minute = 0; double second = 0; @@ -137,13 +142,13 @@ struct TimeRecord final { * ToTemporalTimeRecord ( temporalTimeLike [ , completeness ] ) */ bool ToTemporalTimeRecord(JSContext* cx, JS::Handle temporalTimeLike, - TimeRecord* result); + TemporalTimeLike* result); /** * RegulateTime ( hour, minute, second, millisecond, microsecond, nanosecond, * overflow ) */ -bool RegulateTime(JSContext* cx, const TimeRecord& time, +bool RegulateTime(JSContext* cx, const TemporalTimeLike& time, TemporalOverflow overflow, PlainTime* result); /** @@ -169,19 +174,11 @@ struct RoundedTime final { /** * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, - * increment, unit, roundingMode [ , dayLengthNs ] ) + * increment, unit, roundingMode ) */ RoundedTime RoundTime(const PlainTime& time, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode); -/** - * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, - * increment, unit, roundingMode [ , dayLengthNs ] ) - */ -RoundedTime RoundTime(const PlainTime& time, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - const InstantSpan& dayLengthNs); - } /* namespace js::temporal */ #endif /* builtin_temporal_PlainTime_h */ diff --git a/js/src/builtin/temporal/PlainYearMonth.cpp b/js/src/builtin/temporal/PlainYearMonth.cpp index b95efd3179..c28277e2be 100644 --- a/js/src/builtin/temporal/PlainYearMonth.cpp +++ b/js/src/builtin/temporal/PlainYearMonth.cpp @@ -111,7 +111,7 @@ static PlainYearMonthObject* CreateTemporalYearMonth( // testing |referenceISODay|? // Step 2. - if (!ISOYearMonthWithinLimits(isoYear, isoMonth)) { + if (!ISOYearMonthWithinLimits(isoYear, int32_t(isoMonth))) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID); return nullptr; @@ -130,16 +130,19 @@ static PlainYearMonthObject* CreateTemporalYearMonth( } // Step 5. - obj->setFixedSlot(PlainYearMonthObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + obj->setFixedSlot(PlainYearMonthObject::ISO_YEAR_SLOT, + Int32Value(int32_t(isoYear))); // Step 6. - obj->setFixedSlot(PlainYearMonthObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + obj->setFixedSlot(PlainYearMonthObject::ISO_MONTH_SLOT, + Int32Value(int32_t(isoMonth))); // Step 7. obj->setFixedSlot(PlainYearMonthObject::CALENDAR_SLOT, calendar.toValue()); // Step 8. - obj->setFixedSlot(PlainYearMonthObject::ISO_DAY_SLOT, Int32Value(isoDay)); + obj->setFixedSlot(PlainYearMonthObject::ISO_DAY_SLOT, + Int32Value(int32_t(isoDay))); // Step 9. return obj; @@ -151,7 +154,7 @@ static PlainYearMonthObject* CreateTemporalYearMonth( */ PlainYearMonthObject* js::temporal::CreateTemporalYearMonth( JSContext* cx, const PlainDate& date, Handle calendar) { - auto& [isoYear, isoMonth, isoDay] = date; + const auto& [isoYear, isoMonth, isoDay] = date; // Step 1. if (!ThrowIfInvalidISODate(cx, date)) { @@ -251,8 +254,8 @@ static Wrapped ToTemporalYearMonth( // Step 3.f. if (maybeResolvedOptions) { - return CalendarYearMonthFromFields(cx, calendar, fields, - maybeResolvedOptions); + return temporal::CalendarYearMonthFromFields(cx, calendar, fields, + maybeResolvedOptions); } return CalendarYearMonthFromFields(cx, calendar, fields); } @@ -420,9 +423,6 @@ static bool DifferenceTemporalPlainYearMonth(JSContext* cx, } // Step 8. - // FIXME: spec issue - duplicate CreateDataPropertyOrThrow for "largestUnit". - - // Step 9. Rooted calendarRec(cx); if (!CreateCalendarMethodsRecord(cx, calendar, { @@ -435,7 +435,7 @@ static bool DifferenceTemporalPlainYearMonth(JSContext* cx, return false; } - // Step 10. + // Step 9. JS::RootedVector fieldNames(cx); if (!CalendarFields(cx, calendarRec, {CalendarField::MonthCode, CalendarField::Year}, @@ -443,101 +443,92 @@ static bool DifferenceTemporalPlainYearMonth(JSContext* cx, return false; } - // Step 11. + // Step 10. Rooted thisFields( cx, PrepareTemporalFields(cx, yearMonth, fieldNames)); if (!thisFields) { return false; } - // Step 12. + // Step 11. Value one = Int32Value(1); auto handleOne = Handle::fromMarkedLocation(&one); if (!DefineDataProperty(cx, thisFields, cx->names().day, handleOne)) { return false; } - // Step 13. + // Step 12. Rooted> thisDate( cx, CalendarDateFromFields(cx, calendarRec, thisFields)); if (!thisDate) { return false; } - // Step 14. + // Step 13. Rooted otherFields( cx, PrepareTemporalFields(cx, other, fieldNames)); if (!otherFields) { return false; } - // Step 15. + // Step 14. if (!DefineDataProperty(cx, otherFields, cx->names().day, handleOne)) { return false; } - // Step 16. + // Step 15. Rooted> otherDate( cx, CalendarDateFromFields(cx, calendarRec, otherFields)); if (!otherDate) { return false; } - // Steps 17-18. - Duration result; + // Steps 16-17. + DateDuration until; if (resolvedOptions) { - // Step 17. - Rooted largestUnitValue( - cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); - if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, - largestUnitValue)) { - return false; - } - - // Step 18. if (!CalendarDateUntil(cx, calendarRec, thisDate, otherDate, - resolvedOptions, &result)) { + settings.largestUnit, resolvedOptions, &until)) { return false; } } else { - // Steps 17-18. if (!CalendarDateUntil(cx, calendarRec, thisDate, otherDate, - settings.largestUnit, &result)) { + settings.largestUnit, &until)) { return false; } } // We only care about years and months here, all other fields are set to zero. - Duration duration = {result.years, result.months}; + auto dateDuration = DateDuration{until.years, until.months}; - // Step 19. + // Step 18. if (settings.smallestUnit != TemporalUnit::Month || settings.roundingIncrement != Increment{1}) { - // Steps 19.a-b. - Duration roundResult; - if (!RoundDuration(cx, duration, settings.roundingIncrement, + // Steps 18.a-b. + NormalizedDuration roundResult; + if (!RoundDuration(cx, {dateDuration, {}}, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, thisDate, calendarRec, &roundResult)) { return false; } - // Step 19.c. - auto toBalance = Duration{roundResult.years, roundResult.months}; - DateDuration balanceResult; + // Step 18.c. + auto toBalance = + DateDuration{roundResult.date.years, roundResult.date.months}; if (!temporal::BalanceDateDurationRelative( cx, toBalance, settings.largestUnit, settings.smallestUnit, - thisDate, calendarRec, &balanceResult)) { + thisDate, calendarRec, &dateDuration)) { return false; } - duration = balanceResult.toDuration(); } - // Step 20. + // Step 19. + auto duration = + Duration{double(dateDuration.years), double(dateDuration.months)}; if (operation == TemporalDifference::Since) { duration = duration.negate(); } - auto* obj = CreateTemporalDuration(cx, {duration.years, duration.months}); + auto* obj = CreateTemporalDuration(cx, duration); if (!obj) { return false; } @@ -569,16 +560,37 @@ static bool AddDurationToOrSubtractDurationFromPlainYearMonth( } // Step 3. - TimeDuration balanceResult; - if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &balanceResult)) { + Rooted options(cx); + if (args.hasDefined(1)) { + const char* name = + operation == PlainYearMonthDuration::Add ? "add" : "subtract"; + options = RequireObjectArg(cx, "options", name, args[1]); + } else { + // TODO: Avoid creating an options object if not necessary. + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { return false; } // Step 4. - int32_t sign = DurationSign( - {duration.years, duration.months, duration.weeks, balanceResult.days}); + auto timeDuration = NormalizeTimeDuration(duration); // Step 5. + auto balancedTime = BalanceTimeDuration(timeDuration, TemporalUnit::Day); + + // Steps 6 and 16. (Reordered) + auto durationToAdd = DateDuration{ + int64_t(duration.years), + int64_t(duration.months), + int64_t(duration.weeks), + int64_t(duration.days) + balancedTime.days, + }; + + // Step 7. + int32_t sign = DurationSign(durationToAdd); + + // Step 8. Rooted calendarValue(cx, yearMonth->calendar()); Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, calendarValue, @@ -593,7 +605,7 @@ static bool AddDurationToOrSubtractDurationFromPlainYearMonth( return false; }; - // Step 6. + // Step 9. JS::RootedVector fieldNames(cx); if (!CalendarFields(cx, calendar, {CalendarField::MonthCode, CalendarField::Year}, @@ -601,34 +613,34 @@ static bool AddDurationToOrSubtractDurationFromPlainYearMonth( return false; } - // Step 7. + // Step 10. Rooted fields(cx, PrepareTemporalFields(cx, yearMonth, fieldNames)); if (!fields) { return false; } - // Step 8. + // Step 11. Rooted fieldsCopy(cx, SnapshotOwnProperties(cx, fields)); if (!fieldsCopy) { return false; } - // Step 9. + // Step 12. Value one = Int32Value(1); auto handleOne = Handle::fromMarkedLocation(&one); if (!DefineDataProperty(cx, fields, cx->names().day, handleOne)) { return false; } - // Step 10. + // Step 13. Rooted> intermediateDate( cx, CalendarDateFromFields(cx, calendar, fields)); if (!intermediateDate) { return false; } - // Steps 11-12. + // Steps 14-15. Rooted> date(cx); if (sign < 0) { // |intermediateDate| is initialized to the first day of |yearMonth|'s @@ -645,10 +657,10 @@ static bool AddDurationToOrSubtractDurationFromPlainYearMonth( // some days are skipped, for example consider the Julian-to-Gregorian // calendar transition. - // Step 11.a. - Duration oneMonthDuration = {0, 1}; + // Step 14.a. + auto oneMonthDuration = DateDuration{0, 1}; - // Step 11.b. + // Step 14.b. Rooted> nextMonth( cx, CalendarDateAdd(cx, calendar, intermediateDate, oneMonthDuration)); if (!nextMonth) { @@ -661,87 +673,63 @@ static bool AddDurationToOrSubtractDurationFromPlainYearMonth( } auto nextMonthDate = ToPlainDate(unwrappedNextMonth); - // Step 11.c. - PlainDate endOfMonthISO; - if (!AddISODate(cx, nextMonthDate, {0, 0, 0, -1}, - TemporalOverflow::Constrain, &endOfMonthISO)) { - return false; - } + // Step 14.c. + auto endOfMonthISO = BalanceISODate(nextMonthDate.year, nextMonthDate.month, + nextMonthDate.day - 1); - // Step 11.d. + // Step 14.d. Rooted endOfMonth(cx); if (!CreateTemporalDate(cx, endOfMonthISO, calendar.receiver(), &endOfMonth)) { return false; } - // Step 11.e. + // Step 14.e. Rooted day(cx); if (!CalendarDay(cx, calendar, endOfMonth.date(), &day)) { return false; } - // Step 11.f. + // Step 14.f. if (!DefineDataProperty(cx, fieldsCopy, cx->names().day, day)) { return false; } - // Step 11.g. + // Step 14.g. date = CalendarDateFromFields(cx, calendar, fieldsCopy); if (!date) { return false; } } else { - // Step 12.a. + // Step 15.a. date = intermediateDate; } - // Step 13. - Duration durationToAdd = {duration.years, duration.months, duration.weeks, - balanceResult.days}; - - // FIXME: spec issue - GetOptionsObject should be called after - // ToTemporalDurationRecord to validate the input type before performing any - // other user-visible operations. - // https://github.com/tc39/proposal-temporal/issues/2721 - - // Step 14. - Rooted options(cx); - if (args.hasDefined(1)) { - const char* name = - operation == PlainYearMonthDuration::Add ? "add" : "subtract"; - options = RequireObjectArg(cx, "options", name, args[1]); - } else { - // TODO: Avoid creating an options object if not necessary. - options = NewPlainObjectWithProto(cx, nullptr); - } - if (!options) { - return false; - } + // Step 16. (Moved above) - // Step 15. + // Step 17. Rooted optionsCopy(cx, SnapshotOwnProperties(cx, options)); if (!optionsCopy) { return false; } - // Step 16. + // Step 18. Rooted> addedDate( cx, AddDate(cx, calendar, date, durationToAdd, options)); if (!addedDate) { return false; } - // Step 17. + // Step 19. Rooted addedDateFields( cx, PrepareTemporalFields(cx, addedDate, fieldNames)); if (!addedDateFields) { return false; } - // Step 18. - auto obj = - CalendarYearMonthFromFields(cx, calendar, addedDateFields, optionsCopy); + // Step 20. + auto obj = temporal::CalendarYearMonthFromFields( + cx, calendar, addedDateFields, optionsCopy); if (!obj) { return false; } @@ -1081,13 +1069,11 @@ static bool PlainYearMonth_with(JSContext* cx, const CallArgs& args) { if (!temporalYearMonthLike) { return false; } - - // Step 4. - if (!RejectTemporalLikeObject(cx, temporalYearMonthLike)) { + if (!ThrowIfTemporalLikeObject(cx, temporalYearMonthLike)) { return false; } - // Step 5. + // Step 4. Rooted resolvedOptions(cx); if (args.hasDefined(1)) { Rooted options(cx, @@ -1103,7 +1089,7 @@ static bool PlainYearMonth_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 6. + // Step 5. Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, calendarValue, { @@ -1115,7 +1101,7 @@ static bool PlainYearMonth_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 7. + // Step 6. JS::RootedVector fieldNames(cx); if (!CalendarFields( cx, calendar, @@ -1124,35 +1110,36 @@ static bool PlainYearMonth_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 8. + // Step 7. Rooted fields(cx, PrepareTemporalFields(cx, yearMonth, fieldNames)); if (!fields) { return false; } - // Step 9. + // Step 8. Rooted partialYearMonth( cx, PreparePartialTemporalFields(cx, temporalYearMonthLike, fieldNames)); if (!partialYearMonth) { return false; } - // Step 10. + // Step 9. Rooted mergedFields( cx, CalendarMergeFields(cx, calendar, fields, partialYearMonth)); if (!mergedFields) { return false; } - // Step 11. + // Step 10. fields = PrepareTemporalFields(cx, mergedFields, fieldNames); if (!fields) { return false; } - // Step 12. - auto obj = CalendarYearMonthFromFields(cx, calendar, fields, resolvedOptions); + // Step 11. + auto obj = temporal::CalendarYearMonthFromFields(cx, calendar, fields, + resolvedOptions); if (!obj) { return false; } @@ -1466,20 +1453,7 @@ static bool PlainYearMonth_toPlainDate(JSContext* cx, const CallArgs& args) { } // Step 12. - Rooted options(cx, NewPlainObjectWithProto(cx, nullptr)); - if (!options) { - return false; - } - - // Step 13. - Rooted overflow(cx, StringValue(cx->names().constrain)); - if (!DefineDataProperty(cx, options, cx->names().overflow, overflow)) { - return false; - } - - // Step 14. - auto obj = CalendarDateFromFields(cx, calendar, mergedFromConcatenatedFields, - options); + auto obj = CalendarDateFromFields(cx, calendar, mergedFromConcatenatedFields); if (!obj) { return false; } diff --git a/js/src/builtin/temporal/Temporal.cpp b/js/src/builtin/temporal/Temporal.cpp index 3960a2832d..f729a7ce0f 100644 --- a/js/src/builtin/temporal/Temporal.cpp +++ b/js/src/builtin/temporal/Temporal.cpp @@ -6,8 +6,10 @@ #include "builtin/temporal/Temporal.h" +#include "mozilla/Casting.h" #include "mozilla/CheckedInt.h" #include "mozilla/Likely.h" +#include "mozilla/MathAlgorithms.h" #include "mozilla/Maybe.h" #include @@ -15,8 +17,10 @@ #include #include #include +#include #include #include +#include #include #include "jsfriendapi.h" @@ -25,6 +29,7 @@ #include "NamespaceImports.h" #include "builtin/temporal/Instant.h" +#include "builtin/temporal/Int128.h" #include "builtin/temporal/PlainDate.h" #include "builtin/temporal/PlainDateTime.h" #include "builtin/temporal/PlainMonthDay.h" @@ -47,7 +52,6 @@ #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" @@ -137,16 +141,16 @@ static bool GetNumberOption(JSContext* cx, Handle options, bool js::temporal::ToTemporalRoundingIncrement(JSContext* cx, Handle options, Increment* increment) { - // Step 1. + // Steps 1-3. double number = 1; if (!GetNumberOption(cx, options, cx->names().roundingIncrement, &number)) { return false; } - // Step 3. (Reordered) + // Step 5. (Reordered) number = std::trunc(number); - // Steps 2 and 4. + // Steps 4 and 6. if (!std::isfinite(number) || number < 1 || number > 1'000'000'000) { ToCStringBuf cbuf; const char* numStr = NumberToCString(&cbuf, number); @@ -157,6 +161,7 @@ bool js::temporal::ToTemporalRoundingIncrement(JSContext* cx, return false; } + // Step 7. *increment = Increment{uint32_t(number)}; return true; } @@ -177,7 +182,7 @@ bool js::temporal::ValidateTemporalRoundingIncrement(JSContext* cx, // Steps 3-4. if (increment.value() > maximum || dividend % increment.value() != 0) { Int32ToCStringBuf cbuf; - const char* numStr = Int32ToCString(&cbuf, increment.value()); + const char* numStr = Int32ToCString(&cbuf, int32_t(increment.value())); // TODO: Better error message could be helpful. JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, @@ -394,7 +399,7 @@ bool js::temporal::GetTemporalUnit(JSContext* cx, Handle value, bool js::temporal::ToTemporalRoundingMode(JSContext* cx, Handle options, TemporalRoundingMode* mode) { - // Step 1. + // Steps 1-2. Rooted string(cx); if (!GetStringOption(cx, options, cx->names().roundingMode, &string)) { return false; @@ -439,556 +444,339 @@ bool js::temporal::ToTemporalRoundingMode(JSContext* cx, 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"); +#ifdef DEBUG +template +static bool IsValidMul(const T& x, const T& y) { + return (mozilla::CheckedInt(x) * y).isValid(); } -static BigInt* Divide(JSContext* cx, Handle dividend, - Handle divisor, - TemporalRoundingMode roundingMode) { - MOZ_ASSERT(!divisor->isNegative()); - MOZ_ASSERT(!divisor->isZero()); +// 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; - 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; + if (x == Int128{0} || y == Int128{0}) { + return true; } - - // Step 9. - Rooted inc(cx, BigInt::createFromInt64(cx, increment)); - if (!inc) { - return nullptr; + if (x > Int128{0}) { + return y > Int128{0} ? x <= max / y : y >= min / x; } - 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); + return y > Int128{0} ? x >= min / y : y >= max / x; } +#endif /** * 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; - } +Int128 js::temporal::RoundNumberToIncrement(int64_t numerator, + int64_t denominator, + Increment increment, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(denominator > 0); + MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); // Dividing zero is always zero. - if (x == Instant{}) { - *result = x; - return true; + if (numerator == 0) { + return Int128{0}; + } + + // We don't have to adjust the divisor when |increment=1|. + if (increment == Increment{1}) { + // Steps 1-8 and implicit step 9. + return Int128{Divide(numerator, denominator, roundingMode)}; } // Fast-path when we can perform the whole computation with int64 values. - if (auto num = x.toNanoseconds(); MOZ_LIKELY(num.isValid())) { + auto divisor = mozilla::CheckedInt64(denominator) * increment.value(); + if (MOZ_LIKELY(divisor.isValid())) { + MOZ_ASSERT(divisor.value() > 0); + // Steps 1-8. - int64_t rounded = Divide(num.value(), increment, roundingMode); + int64_t rounded = Divide(numerator, divisor.value(), roundingMode); // Step 9. - mozilla::CheckedInt64 checked = rounded; - checked *= increment; - if (MOZ_LIKELY(checked.isValid())) { - *result = Instant::fromNanoseconds(checked.value()); - return true; + auto result = mozilla::CheckedInt64(rounded) * increment.value(); + if (MOZ_LIKELY(result.isValid())) { + return Int128{result.value()}; } } - 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; + // Int128 path on overflow. + return RoundNumberToIncrement(Int128{numerator}, Int128{denominator}, + increment, roundingMode); } /** * 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); +Int128 js::temporal::RoundNumberToIncrement(const Int128& numerator, + const Int128& denominator, + Increment increment, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(denominator > Int128{0}); 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; - } + auto inc = Int128{increment.value()}; + MOZ_ASSERT(IsValidMul(denominator, inc), "unsupported overflow"); - 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; - } + auto divisor = denominator * inc; + MOZ_ASSERT(divisor > Int128{0}); // Steps 1-8. - int64_t rounded = Divide(numerator, divisor, roundingMode); + auto 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); + MOZ_ASSERT(IsValidMul(rounded, inc), "unsupported overflow"); + return rounded * inc; } /** * 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()); +Int128 js::temporal::RoundNumberToIncrement(const Int128& x, + const Int128& increment, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(increment > Int128{0}); - // 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-8. + auto rounded = Divide(x, increment, roundingMode); - // Steps 1-9. - auto* rounded = RoundNumberToIncrementSlow(cx, numerator, divisor, - increment.value(), roundingMode); - if (!rounded) { - return false; - } + // Step 9. + MOZ_ASSERT(IsValidMul(rounded, increment), "unsupported overflow"); + return rounded * increment; +} - *result = BigInt::numberValue(rounded); - return true; +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; } /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * 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. */ -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; +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); } - } + }; - // Slow path on overflow. + using UnsignedT = decltype(absValue(T{0})); + static_assert(!std::numeric_limits::is_signed); - Rooted bi(cx, BigInt::createFromInt64(cx, numerator)); - if (!bi) { - return false; - } + 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}; + } + }; - Rooted denom(cx, BigInt::createFromInt64(cx, denominator)); - if (!denom) { - return false; + 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; + } + } + } } - return RoundNumberToIncrement(cx, bi, denom, increment, roundingMode, result); + 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); } -/** - * 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()); +double js::temporal::FractionToDouble(int64_t numerator, int64_t denominator) { + MOZ_ASSERT(denominator > 0); - // Dividing zero is always zero. - if (numerator->isZero()) { - *result = 0; - return true; + // Zero divided by any divisor is still zero. + if (numerator == 0) { + return 0; } - // 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; + // 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); } - Rooted inc(cx, BigInt::createFromUint64(cx, increment.value())); - if (!inc) { - return false; + // 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}); +} - Rooted divisor(cx, BigInt::mul(cx, denominator, inc)); - if (!divisor) { - return false; - } - MOZ_ASSERT(!divisor->isNegative()); - MOZ_ASSERT(!divisor->isZero()); +double js::temporal::FractionToDouble(const Int128& numerator, + const Int128& denominator) { + MOZ_ASSERT(denominator > Int128{0}); - // Steps 1-8. - Rooted rounded(cx, Divide(cx, numerator, divisor, roundingMode)); - if (!rounded) { - return false; + // Zero divided by any divisor is still zero. + if (numerator == Int128{0}) { + return 0; } - // Step 9. - auto* adjusted = BigInt::mul(cx, rounded, inc); - if (!adjusted) { - return false; + // 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); } - *result = BigInt::numberValue(adjusted); - return true; + // Otherwise call into |FractionToDoubleSlow| to compute the exact result. + return FractionToDoubleSlow(numerator, denominator); } /** @@ -1404,17 +1192,14 @@ static JSObject* MaybeUnwrapIf(JSObject* 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 ) + * IsPartialTemporalObject ( object ) */ -bool js::temporal::RejectTemporalLikeObject(JSContext* cx, - Handle object) { - // Step 1. +bool js::temporal::ThrowIfTemporalLikeObject(JSContext* cx, + Handle object) { + // Step 1. (Handled in caller) + + // Step 2. if (auto* unwrapped = MaybeUnwrapIf property(cx); - // Step 2. + // Step 3. if (!GetProperty(cx, object, object, cx->names().calendar, &property)) { return false; } - // Step 3. + // Step 4. if (!property.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "calendar"); return false; } - // Step 4. + // Step 5. if (!GetProperty(cx, object, object, cx->names().timeZone, &property)) { return false; } - // Step 5. + // Step 6. if (!property.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "timeZone"); return false; } - // Step 6. + // Step 7. return true; } @@ -1813,10 +1598,18 @@ static bool TemporalClassFinish(JSContext* cx, Handle temporal, }; // 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}) { + 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; } diff --git a/js/src/builtin/temporal/Temporal.h b/js/src/builtin/temporal/Temporal.h index 3d014bfaa7..8955c1d88a 100644 --- a/js/src/builtin/temporal/Temporal.h +++ b/js/src/builtin/temporal/Temporal.h @@ -13,6 +13,7 @@ #include "jstypes.h" +#include "builtin/temporal/Int128.h" #include "builtin/temporal/TemporalRoundingMode.h" #include "builtin/temporal/TemporalUnit.h" #include "js/RootingAPI.h" @@ -35,9 +36,6 @@ class TemporalObject : public NativeObject { static const ClassSpec classSpec_; }; -struct Instant; -struct PlainTime; - /** * Rounding increment, which is an integer in the range [1, 1'000'000'000]. * @@ -171,37 +169,32 @@ bool ToTemporalRoundingMode(JSContext* cx, JS::Handle options, /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ -bool RoundNumberToIncrement(JSContext* cx, const Instant& x, int64_t increment, - TemporalRoundingMode roundingMode, Instant* result); +Int128 RoundNumberToIncrement(int64_t numerator, int64_t denominator, + Increment increment, + TemporalRoundingMode roundingMode); /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ -bool RoundNumberToIncrement(JSContext* cx, int64_t numerator, TemporalUnit unit, - Increment increment, - TemporalRoundingMode roundingMode, double* result); +Int128 RoundNumberToIncrement(const Int128& numerator, + const Int128& denominator, Increment increment, + TemporalRoundingMode roundingMode); /** * RoundNumberToIncrement ( x, increment, roundingMode ) */ -bool RoundNumberToIncrement(JSContext* cx, JS::Handle numerator, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, double* result); +Int128 RoundNumberToIncrement(const Int128& x, const Int128& increment, + TemporalRoundingMode roundingMode); /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * Return the double value of the fractional number `numerator / denominator`. */ -bool RoundNumberToIncrement(JSContext* cx, int64_t numerator, - int64_t denominator, Increment increment, - TemporalRoundingMode roundingMode, double* result); +double FractionToDouble(int64_t numerator, int64_t denominator); /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * Return the double value of the fractional number `numerator / denominator`. */ -bool RoundNumberToIncrement(JSContext* cx, JS::Handle numerator, - JS::Handle denominator, - Increment increment, - TemporalRoundingMode roundingMode, double* result); +double FractionToDouble(const Int128& numerator, const Int128& denominator); enum class CalendarOption { Auto, Always, Never, Critical }; @@ -221,7 +214,7 @@ class Precision final { constexpr Precision(int8_t value, Tag) : value_(value) {} public: - constexpr explicit Precision(uint8_t value) : value_(value) { + constexpr explicit Precision(uint8_t value) : value_(int8_t(value)) { MOZ_ASSERT(value < 10); } @@ -306,9 +299,12 @@ bool ToShowOffsetOption(JSContext* cx, JS::Handle options, ShowOffsetOption* result); /** - * RejectTemporalLikeObject ( object ) + * IsPartialTemporalObject ( object ) + * + * Our implementation performs error reporting in this function instead of in + * the caller to provide better error messages. */ -bool RejectTemporalLikeObject(JSContext* cx, JS::Handle object); +bool ThrowIfTemporalLikeObject(JSContext* cx, JS::Handle object); /** * ToPositiveIntegerWithTruncation ( argument ) diff --git a/js/src/builtin/temporal/TemporalFields.cpp b/js/src/builtin/temporal/TemporalFields.cpp index 9ac2e44639..72027123e7 100644 --- a/js/src/builtin/temporal/TemporalFields.cpp +++ b/js/src/builtin/temporal/TemporalFields.cpp @@ -214,7 +214,7 @@ static Value TemporalFieldDefaultValue(TemporalField field) { static bool TemporalFieldConvertValue(JSContext* cx, TemporalField field, MutableHandle value) { - auto* name = ToCString(field); + const auto* name = ToCString(field); switch (field) { case TemporalField::Year: case TemporalField::Hour: @@ -365,7 +365,7 @@ bool js::temporal::PrepareTemporalFields( Rooted value(cx); for (auto fieldName : fieldNames) { auto* property = ToPropertyName(cx, fieldName); - auto* cstr = ToCString(fieldName); + const auto* cstr = ToCString(fieldName); // Step 6.a. (Not applicable in our implementation.) @@ -837,8 +837,8 @@ bool js::temporal::AppendSorted( return false; } - auto* left = std::prev(fieldNames.end(), additionalNames.size()); - auto* right = additionalNames.end(); + const auto* left = std::prev(fieldNames.end(), additionalNames.size()); + const auto* right = additionalNames.end(); auto* out = fieldNames.end(); // Write backwards into the newly allocated space. diff --git a/js/src/builtin/temporal/TemporalFields.h b/js/src/builtin/temporal/TemporalFields.h index 1af3631169..4cdbade772 100644 --- a/js/src/builtin/temporal/TemporalFields.h +++ b/js/src/builtin/temporal/TemporalFields.h @@ -7,6 +7,7 @@ #ifndef builtin_temporal_TemporalFields_h #define builtin_temporal_TemporalFields_h +#include "mozilla/Attributes.h" #include "mozilla/FloatingPoint.h" #include @@ -46,7 +47,7 @@ enum class TemporalField { // NaN whereas pointer fields use nullptr. // // [1] -struct TemporalFields final { +struct MOZ_STACK_CLASS TemporalFields final { double year = mozilla::UnspecifiedNaN(); double month = mozilla::UnspecifiedNaN(); JSString* monthCode = nullptr; diff --git a/js/src/builtin/temporal/TemporalNow.cpp b/js/src/builtin/temporal/TemporalNow.cpp index adc84361af..3b06736673 100644 --- a/js/src/builtin/temporal/TemporalNow.cpp +++ b/js/src/builtin/temporal/TemporalNow.cpp @@ -116,9 +116,9 @@ static JSString* SystemTimeZoneIdentifier(JSContext* cx) { size_t n = etcGMT.copy(offsetString, etcGMT.length()); offsetString[n++] = offset < 0 ? '+' : '-'; if (offsetHours >= 10) { - offsetString[n++] = '0' + (offsetHours / 10); + offsetString[n++] = char('0' + (offsetHours / 10)); } - offsetString[n++] = '0' + (offsetHours % 10); + offsetString[n++] = char('0' + (offsetHours % 10)); MOZ_ASSERT(n == etcGMT.length() + 2 || n == etcGMT.length() + 3); diff --git a/js/src/builtin/temporal/TemporalParser.cpp b/js/src/builtin/temporal/TemporalParser.cpp index c117e63b31..b3a7a58c62 100644 --- a/js/src/builtin/temporal/TemporalParser.cpp +++ b/js/src/builtin/temporal/TemporalParser.cpp @@ -80,11 +80,11 @@ bool EqualCharIgnoreCaseAscii(CharT c1, char c2) { // Convert both characters to lower case before the comparison. char c = c1; if (mozilla::IsAsciiUppercaseAlpha(c1)) { - c = c + toLower; + c = char(c + toLower); } char d = c2; if (mozilla::IsAsciiUppercaseAlpha(c2)) { - d = d + toLower; + d = char(d + toLower); } return c == d; } @@ -544,10 +544,10 @@ class TemporalParser final { return mozilla::Some(num); } - // TimeFractionalPart : + // TimeFractionalPart ::: // Digit{1, 9} // - // Fraction : + // Fraction ::: // DecimalSeparator TimeFractionalPart mozilla::Maybe fraction() { if (!reader_.hasMore(2)) { @@ -679,11 +679,11 @@ class TemporalParser final { return true; } - // Sign : + // Sign ::: // ASCIISign // U+2212 // - // ASCIISign : one of + // ASCIISign ::: one of // + - bool hasSign() const { return hasOneOf({'+', '-', 0x2212}); } @@ -698,61 +698,61 @@ class TemporalParser final { return plus ? 1 : -1; } - // DecimalSeparator : one of + // DecimalSeparator ::: one of // . , bool hasDecimalSeparator() const { return hasOneOf({'.', ','}); } bool decimalSeparator() { return oneOf({'.', ','}); } - // DaysDesignator : one of + // DaysDesignator ::: one of // D d bool daysDesignator() { return oneOf({'D', 'd'}); } - // HoursDesignator : one of + // HoursDesignator ::: one of // H h bool hoursDesignator() { return oneOf({'H', 'h'}); } - // MinutesDesignator : one of + // MinutesDesignator ::: one of // M m bool minutesDesignator() { return oneOf({'M', 'm'}); } - // MonthsDesignator : one of + // MonthsDesignator ::: one of // M m bool monthsDesignator() { return oneOf({'M', 'm'}); } - // DurationDesignator : one of + // DurationDesignator ::: one of // P p bool durationDesignator() { return oneOf({'P', 'p'}); } - // SecondsDesignator : one of + // SecondsDesignator ::: one of // S s bool secondsDesignator() { return oneOf({'S', 's'}); } - // DateTimeSeparator : + // DateTimeSeparator ::: // // T // t bool dateTimeSeparator() { return oneOf({' ', 'T', 't'}); } - // TimeDesignator : one of + // TimeDesignator ::: one of // T t bool hasTimeDesignator() const { return hasOneOf({'T', 't'}); } bool timeDesignator() { return oneOf({'T', 't'}); } - // WeeksDesignator : one of + // WeeksDesignator ::: one of // W w bool weeksDesignator() { return oneOf({'W', 'w'}); } - // YearsDesignator : one of + // YearsDesignator ::: one of // Y y bool yearsDesignator() { return oneOf({'Y', 'y'}); } - // UTCDesignator : one of + // UTCDesignator ::: one of // Z z bool utcDesignator() { return oneOf({'Z', 'z'}); } - // TZLeadingChar : + // TZLeadingChar ::: // Alpha // . // _ @@ -762,7 +762,7 @@ class TemporalParser final { }); } - // TZChar : + // TZChar ::: // TZLeadingChar // DecimalDigit // - @@ -774,11 +774,11 @@ class TemporalParser final { }); } - // AnnotationCriticalFlag : + // AnnotationCriticalFlag ::: // ! bool annotationCriticalFlag() { return character('!'); } - // AKeyLeadingChar : + // AKeyLeadingChar ::: // LowercaseAlpha // _ bool aKeyLeadingChar() { @@ -787,7 +787,7 @@ class TemporalParser final { }); } - // AKeyChar : + // AKeyChar ::: // AKeyLeadingChar // DecimalDigit // - @@ -798,7 +798,7 @@ class TemporalParser final { }); } - // AnnotationValueComponent : + // AnnotationValueComponent ::: // Alpha AnnotationValueComponent? // DecimalDigit AnnotationValueComponent? bool annotationValueComponent() { @@ -929,7 +929,7 @@ class TemporalParser final { template mozilla::Result TemporalParser::dateTime() { - // DateTime : + // DateTime ::: // Date // Date DateTimeSeparator TimeSpec DateTimeUTCOffset? ZonedDateTimeString result = {}; @@ -961,12 +961,12 @@ TemporalParser::dateTime() { template mozilla::Result TemporalParser::date() { - // Date : + // Date ::: // DateYear - DateMonth - DateDay // DateYear DateMonth DateDay PlainDate result = {}; - // DateYear : + // DateYear ::: // DecimalDigit{4} // Sign DecimalDigit{6} if (auto year = digits(4)) { @@ -988,7 +988,7 @@ mozilla::Result TemporalParser::date() { // Optional: - character('-'); - // DateMonth : + // DateMonth ::: // 0 NonzeroDigit // 10 // 11 @@ -1005,7 +1005,7 @@ mozilla::Result TemporalParser::date() { // Optional: - character('-'); - // DateDay : + // DateDay ::: // 0 NonzeroDigit // 1 DecimalDigit // 2 DecimalDigit @@ -1025,7 +1025,7 @@ mozilla::Result TemporalParser::date() { template mozilla::Result TemporalParser::timeSpec() { - // TimeSpec : + // TimeSpec ::: // TimeHour // TimeHour : TimeMinute // TimeHour TimeMinute @@ -1033,12 +1033,11 @@ mozilla::Result TemporalParser::timeSpec() { // TimeHour TimeMinute TimeSecond TimeFraction? PlainTime result = {}; - // TimeHour : - // Hour[+Padded] + // TimeHour ::: + // Hour // - // Hour[Padded] : - // [~Padded] DecimalDigit - // [~Padded] 0 DecimalDigit + // Hour ::: + // 0 DecimalDigit // 1 DecimalDigit // 20 // 21 @@ -1056,10 +1055,10 @@ mozilla::Result TemporalParser::timeSpec() { // Optional: : bool needsMinutes = character(':'); - // TimeMinute : + // TimeMinute ::: // MinuteSecond // - // MinuteSecond : + // MinuteSecond ::: // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit @@ -1075,7 +1074,7 @@ mozilla::Result TemporalParser::timeSpec() { // Optional: : bool needsSeconds = needsMinutes && character(':'); - // TimeSecond : + // TimeSecond ::: // MinuteSecond // 60 if (auto second = digits(2)) { @@ -1084,7 +1083,7 @@ mozilla::Result TemporalParser::timeSpec() { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_LEAPSECOND); } - // TimeFraction : + // TimeFraction ::: // Fraction if (auto f = fraction()) { int32_t fractionalPart = f.value(); @@ -1105,7 +1104,7 @@ mozilla::Result TemporalParser::timeSpec() { template mozilla::Result TemporalParser::dateTimeUTCOffset() { - // DateTimeUTCOffset : + // DateTimeUTCOffset ::: // UTCDesignator // UTCOffsetSubMinutePrecision @@ -1127,13 +1126,13 @@ TemporalParser::dateTimeUTCOffset() { template mozilla::Result TemporalParser::timeZoneUTCOffsetName() { - // TimeZoneUTCOffsetName : + // TimeZoneUTCOffsetName ::: // UTCOffsetMinutePrecision // - // UTCOffsetMinutePrecision : - // Sign Hour[+Padded] - // Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond - // Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond + // UTCOffsetMinutePrecision ::: + // Sign Hour + // Sign Hour TimeSeparator[+Extended] MinuteSecond + // Sign Hour TimeSeparator[~Extended] MinuteSecond TimeZoneUTCOffset result = {}; @@ -1142,9 +1141,8 @@ TemporalParser::timeZoneUTCOffsetName() { } result.sign = sign(); - // Hour[Padded] : - // [~Padded] DecimalDigit - // [+Padded] 0 DecimalDigit + // Hour ::: + // 0 DecimalDigit // 1 DecimalDigit // 20 // 21 @@ -1159,12 +1157,12 @@ TemporalParser::timeZoneUTCOffsetName() { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); } - // TimeSeparator[Extended] : + // TimeSeparator[Extended] ::: // [+Extended] : // [~Extended] [empty] bool needsMinutes = character(':'); - // MinuteSecond : + // MinuteSecond ::: // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit @@ -1192,18 +1190,18 @@ mozilla::Result TemporalParser::utcOffsetSubMinutePrecision() { // clang-format off // - // UTCOffsetSubMinutePrecision : + // UTCOffsetSubMinutePrecision ::: // UTCOffsetMinutePrecision // UTCOffsetWithSubMinuteComponents[+Extended] // UTCOffsetWithSubMinuteComponents[~Extended] // - // UTCOffsetMinutePrecision : - // Sign Hour[+Padded] - // Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond - // Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond + // UTCOffsetMinutePrecision ::: + // Sign Hour + // Sign Hour TimeSeparator[+Extended] MinuteSecond + // Sign Hour TimeSeparator[~Extended] MinuteSecond // - // UTCOffsetWithSubMinuteComponents[Extended] : - // Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction? + // UTCOffsetWithSubMinuteComponents[Extended] ::: + // Sign Hour TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction? // // clang-format on @@ -1214,9 +1212,8 @@ TemporalParser::utcOffsetSubMinutePrecision() { } result.sign = sign(); - // Hour[Padded] : - // [~Padded] DecimalDigit - // [+Padded] 0 DecimalDigit + // Hour ::: + // 0 DecimalDigit // 1 DecimalDigit // 20 // 21 @@ -1231,12 +1228,12 @@ TemporalParser::utcOffsetSubMinutePrecision() { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); } - // TimeSeparator[Extended] : + // TimeSeparator[Extended] ::: // [+Extended] : // [~Extended] [empty] bool needsMinutes = character(':'); - // MinuteSecond : + // MinuteSecond ::: // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit @@ -1249,12 +1246,12 @@ TemporalParser::utcOffsetSubMinutePrecision() { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); } - // TimeSeparator[Extended] : + // TimeSeparator[Extended] ::: // [+Extended] : // [~Extended] [empty] bool needsSeconds = needsMinutes && character(':'); - // MinuteSecond : + // MinuteSecond ::: // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit @@ -1285,7 +1282,7 @@ TemporalParser::utcOffsetSubMinutePrecision() { template mozilla::Result TemporalParser::timeZoneIdentifier() { - // TimeZoneIdentifier : + // TimeZoneIdentifier ::: // TimeZoneUTCOffsetName // TimeZoneIANAName @@ -1310,7 +1307,7 @@ TemporalParser::timeZoneIdentifier() { template mozilla::Result TemporalParser::timeZoneAnnotation() { - // TimeZoneAnnotation : + // TimeZoneAnnotation ::: // [ AnnotationCriticalFlag? TimeZoneIdentifier ] if (!character('[')) { @@ -1335,11 +1332,11 @@ TemporalParser::timeZoneAnnotation() { template mozilla::Result TemporalParser::timeZoneIANAName() { - // TimeZoneIANAName : + // TimeZoneIANAName ::: // TimeZoneIANANameComponent // TimeZoneIANAName / TimeZoneIANANameComponent // - // TimeZoneIANANameComponent : + // TimeZoneIANANameComponent ::: // TZLeadingChar // TimeZoneIANANameComponent TZChar @@ -1385,7 +1382,7 @@ TemporalParser::parseTemporalInstantString() { // clang-format off // - // TemporalInstantString : + // TemporalInstantString ::: // Date DateTimeSeparator TimeSpec DateTimeUTCOffset TimeZoneAnnotation? Annotations? // // clang-format on @@ -1492,7 +1489,7 @@ bool js::temporal::ParseTemporalInstantString(JSContext* cx, template mozilla::Result TemporalParser::parseTemporalTimeZoneString() { - // TimeZoneIdentifier : + // TimeZoneIdentifier ::: // TimeZoneUTCOffsetName // TimeZoneIANAName @@ -1819,10 +1816,10 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { // Initialize all fields to zero. TemporalDurationString result = {}; - // TemporalDurationString : + // TemporalDurationString ::: // Duration // - // Duration : + // Duration ::: // Sign? DurationDesignator DurationDate // Sign? DurationDesignator DurationTime @@ -1834,7 +1831,7 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DESIGNATOR); } - // DurationDate : + // DurationDate ::: // DurationYearsPart DurationTime? // DurationMonthsPart DurationTime? // DurationWeeksPart DurationTime? @@ -1851,12 +1848,12 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { num = *d; } - // DurationYearsPart : + // DurationYearsPart ::: // DurationYears YearsDesignator DurationMonthsPart // DurationYears YearsDesignator DurationWeeksPart // DurationYears YearsDesignator DurationDaysPart? // - // DurationYears : + // DurationYears ::: // DecimalDigits[~Sep] if (yearsDesignator()) { result.years = num; @@ -1873,11 +1870,11 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { } } - // DurationMonthsPart : + // DurationMonthsPart ::: // DurationMonths MonthsDesignator DurationWeeksPart // DurationMonths MonthsDesignator DurationDaysPart? // - // DurationMonths : + // DurationMonths ::: // DecimalDigits[~Sep] if (monthsDesignator()) { result.months = num; @@ -1894,10 +1891,10 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { } } - // DurationWeeksPart : + // DurationWeeksPart ::: // DurationWeeks WeeksDesignator DurationDaysPart? // - // DurationWeeks : + // DurationWeeks ::: // DecimalDigits[~Sep] if (weeksDesignator()) { result.weeks = num; @@ -1914,10 +1911,10 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { } } - // DurationDaysPart : + // DurationDaysPart ::: // DurationDays DaysDesignator // - // DurationDays : + // DurationDays ::: // DecimalDigits[~Sep] if (daysDesignator()) { result.days = num; @@ -1932,7 +1929,7 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } while (false); - // DurationTime : + // DurationTime ::: // DurationTimeDesignator DurationHoursPart // DurationTimeDesignator DurationMinutesPart // DurationTimeDesignator DurationSecondsPart @@ -1958,18 +1955,18 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { // clang-format off // - // DurationHoursPart : + // DurationHoursPart ::: // DurationWholeHours DurationHoursFraction HoursDesignator // DurationWholeHours HoursDesignator DurationMinutesPart // DurationWholeHours HoursDesignator DurationSecondsPart? // - // DurationWholeHours : + // DurationWholeHours ::: // DecimalDigits[~Sep] // - // DurationHoursFraction : + // DurationHoursFraction ::: // TimeFraction // - // TimeFraction : + // TimeFraction ::: // Fraction // // clang-format on @@ -1988,17 +1985,17 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { // clang-format off // - // DurationMinutesPart : + // DurationMinutesPart ::: // DurationWholeMinutes DurationMinutesFraction MinutesDesignator // DurationWholeMinutes MinutesDesignator DurationSecondsPart? // - // DurationWholeMinutes : + // DurationWholeMinutes ::: // DecimalDigits[~Sep] // - // DurationMinutesFraction : + // DurationMinutesFraction ::: // TimeFraction // - // TimeFraction : + // TimeFraction ::: // Fraction // // clang-format on @@ -2018,16 +2015,16 @@ TemporalParser::parseTemporalDurationString(JSContext* cx) { } } - // DurationSecondsPart : + // DurationSecondsPart ::: // DurationWholeSeconds DurationSecondsFraction? SecondsDesignator // - // DurationWholeSeconds : + // DurationWholeSeconds ::: // DecimalDigits[~Sep] // - // DurationSecondsFraction : + // DurationSecondsFraction ::: // TimeFraction // - // TimeFraction : + // TimeFraction ::: // Fraction if (secondsDesignator()) { if (hasHoursFraction || hasMinutesFraction) { @@ -2106,14 +2103,14 @@ bool js::temporal::ParseTemporalDurationString(JSContext* cx, // Steps 9.b-d. int64_t h = int64_t(parsed.hoursFraction) * 60; - minutes = h / 1'000'000'000; + minutes = double(h / 1'000'000'000); // Steps 13 and 15-17. int64_t min = (h % 1'000'000'000) * 60; - seconds = min / 1'000'000'000; - milliseconds = (min % 1'000'000'000) / 1'000'000; - microseconds = (min % 1'000'000) / 1'000; - nanoseconds = (min % 1'000); + seconds = double(min / 1'000'000'000); + milliseconds = double((min % 1'000'000'000) / 1'000'000); + microseconds = double((min % 1'000'000) / 1'000); + nanoseconds = double(min % 1'000); } // Step 11. @@ -2130,10 +2127,10 @@ bool js::temporal::ParseTemporalDurationString(JSContext* cx, // Steps 11.b-d and 15-17. int64_t min = int64_t(parsed.minutesFraction) * 60; - seconds = min / 1'000'000'000; - milliseconds = (min % 1'000'000'000) / 1'000'000; - microseconds = (min % 1'000'000) / 1'000; - nanoseconds = (min % 1'000); + seconds = double(min / 1'000'000'000); + milliseconds = double((min % 1'000'000'000) / 1'000'000); + microseconds = double((min % 1'000'000) / 1'000); + nanoseconds = double(min % 1'000); } // Step 14. @@ -2148,9 +2145,9 @@ bool js::temporal::ParseTemporalDurationString(JSContext* cx, seconds = parsed.seconds; // Steps 14, 16-17 - milliseconds = (parsed.secondsFraction / 1'000'000); - microseconds = ((parsed.secondsFraction % 1'000'000) / 1'000); - nanoseconds = (parsed.secondsFraction % 1'000); + milliseconds = double(parsed.secondsFraction / 1'000'000); + microseconds = double((parsed.secondsFraction % 1'000'000) / 1'000); + nanoseconds = double(parsed.secondsFraction % 1'000); } else { // Step 10. minutes = parsed.minutes; @@ -2168,7 +2165,7 @@ bool js::temporal::ParseTemporalDurationString(JSContext* cx, int32_t factor = parsed.sign ? parsed.sign : 1; MOZ_ASSERT(factor == -1 || factor == 1); - // Step 20. + // Steps 20-29. *result = { (years * factor) + (+0.0), (months * factor) + (+0.0), (weeks * factor) + (+0.0), (days * factor) + (+0.0), @@ -2176,16 +2173,15 @@ bool js::temporal::ParseTemporalDurationString(JSContext* cx, (seconds * factor) + (+0.0), (milliseconds * factor) + (+0.0), (microseconds * factor) + (+0.0), (nanoseconds * factor) + (+0.0), }; - if (!ThrowIfInvalidDuration(cx, *result)) { - return false; - } - return true; + + // Steps 30-31. + return ThrowIfInvalidDuration(cx, *result); } template mozilla::Result TemporalParser::annotationKey() { - // AnnotationKey : + // AnnotationKey ::: // AKeyLeadingChar // AnnotationKey AKeyChar @@ -2205,7 +2201,7 @@ TemporalParser::annotationKey() { template mozilla::Result TemporalParser::annotationValue() { - // AnnotationValue : + // AnnotationValue ::: // AnnotationValueComponent // AnnotationValueComponent - AnnotationValue @@ -2222,7 +2218,7 @@ TemporalParser::annotationValue() { template mozilla::Result TemporalParser::annotation() { - // Annotation : + // Annotation ::: // [ AnnotationCriticalFlag? AnnotationKey = AnnotationValue ] if (!character('[')) { @@ -2255,7 +2251,7 @@ mozilla::Result TemporalParser::annotation() { template mozilla::Result TemporalParser::annotations() { - // Annotations: + // Annotations ::: // Annotation Annotations? MOZ_ASSERT(hasAnnotationStart()); @@ -2269,9 +2265,6 @@ TemporalParser::annotations() { } auto [key, value, critical] = anno.unwrap(); - // FIXME: spec issue - ignore case for "[u-ca=" to match BCP47? - // https://github.com/tc39/proposal-temporal/issues/2524 - static constexpr std::string_view ca = "u-ca"; auto keySpan = reader_.substring(key); @@ -2295,7 +2288,7 @@ mozilla::Result TemporalParser::annotatedTime() { // clang-format off // - // AnnotatedTime : + // AnnotatedTime ::: // TimeDesignator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? // TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation? Annotations? // @@ -2339,7 +2332,7 @@ TemporalParser::annotatedTime() { // clang-format off // - // TimeSpecWithOptionalOffsetNotAmbiguous : + // TimeSpecWithOptionalOffsetNotAmbiguous ::: // TimeSpec DateTimeUTCOffset? but not one of ValidMonthDay or DateSpecYearMonth // // clang-format on @@ -2407,7 +2400,7 @@ TemporalParser::annotatedTime() { template mozilla::Result TemporalParser::annotatedDateTime() { - // AnnotatedDateTime[Zoned] : + // AnnotatedDateTime[Zoned] ::: // [~Zoned] DateTime TimeZoneAnnotation? Annotations? // [+Zoned] DateTime TimeZoneAnnotation Annotations? @@ -2441,7 +2434,7 @@ mozilla::Result TemporalParser::annotatedDateTimeTimeRequired() { // clang-format off // - // AnnotatedDateTimeTimeRequired : + // AnnotatedDateTimeTimeRequired ::: // Date DateTimeSeparator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? // // clang-format on @@ -2494,7 +2487,7 @@ TemporalParser::annotatedDateTimeTimeRequired() { template mozilla::Result TemporalParser::annotatedYearMonth() { - // AnnotatedYearMonth : + // AnnotatedYearMonth ::: // DateSpecYearMonth TimeZoneAnnotation? Annotations? ZonedDateTimeString result = {}; @@ -2527,7 +2520,7 @@ TemporalParser::annotatedYearMonth() { template mozilla::Result TemporalParser::annotatedMonthDay() { - // AnnotatedMonthDay : + // AnnotatedMonthDay ::: // DateSpecMonthDay TimeZoneAnnotation? Annotations? ZonedDateTimeString result = {}; @@ -2560,11 +2553,11 @@ TemporalParser::annotatedMonthDay() { template mozilla::Result TemporalParser::dateSpecYearMonth() { - // DateSpecYearMonth : + // DateSpecYearMonth ::: // DateYear -? DateMonth PlainDate result = {}; - // DateYear : + // DateYear ::: // DecimalDigit{4} // Sign DecimalDigit{6} if (auto year = digits(4)) { @@ -2585,7 +2578,7 @@ TemporalParser::dateSpecYearMonth() { character('-'); - // DateMonth : + // DateMonth ::: // 0 NonzeroDigit // 10 // 11 @@ -2608,16 +2601,17 @@ TemporalParser::dateSpecYearMonth() { template mozilla::Result TemporalParser::dateSpecMonthDay() { - // DateSpecMonthDay : + // DateSpecMonthDay ::: // -- DateMonth -? DateDay // DateMonth -? DateDay PlainDate result = {}; + // Optional: -- string("--"); result.year = AbsentYear; - // DateMonth : + // DateMonth ::: // 0 NonzeroDigit // 10 // 11 @@ -2631,9 +2625,10 @@ TemporalParser::dateSpecMonthDay() { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); } + // Optional: - character('-'); - // DateDay : + // DateDay ::: // 0 NonzeroDigit // 1 DecimalDigit // 2 DecimalDigit @@ -2653,19 +2648,19 @@ TemporalParser::dateSpecMonthDay() { template mozilla::Result TemporalParser::validMonthDay() { - // ValidMonthDay : + // ValidMonthDay ::: // DateMonth -? 0 NonZeroDigit // DateMonth -? 1 DecimalDigit // DateMonth -? 2 DecimalDigit // DateMonth -? 30 but not one of 0230 or 02-30 // DateMonthWithThirtyOneDays -? 31 // - // DateMonthWithThirtyOneDays : one of + // DateMonthWithThirtyOneDays ::: one of // 01 03 05 07 08 10 12 PlainDate result = {}; - // DateMonth : + // DateMonth ::: // 0 NonzeroDigit // 10 // 11 @@ -2679,6 +2674,7 @@ mozilla::Result TemporalParser::validMonthDay() { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); } + // Optional: - character('-'); if (auto day = digits(2)) { @@ -2834,7 +2830,7 @@ JSLinearString* js::temporal::ParseTemporalCalendarString( template mozilla::Result TemporalParser::parseTemporalTimeString() { - // TemporalTimeString : + // TemporalTimeString ::: // AnnotatedTime // AnnotatedDateTimeTimeRequired @@ -2915,7 +2911,7 @@ bool js::temporal::ParseTemporalTimeString(JSContext* cx, Handle str, template mozilla::Result TemporalParser::parseTemporalMonthDayString() { - // TemporalMonthDayString : + // TemporalMonthDayString ::: // AnnotatedMonthDay // AnnotatedDateTime[~Zoned] @@ -3014,7 +3010,7 @@ bool js::temporal::ParseTemporalMonthDayString( template mozilla::Result TemporalParser::parseTemporalYearMonthString() { - // TemporalYearMonthString : + // TemporalYearMonthString ::: // AnnotatedYearMonth // AnnotatedDateTime[~Zoned] @@ -3112,7 +3108,7 @@ bool js::temporal::ParseTemporalYearMonthString( template mozilla::Result TemporalParser::parseTemporalDateTimeString() { - // TemporalDateTimeString[Zoned] : + // TemporalDateTimeString[Zoned] ::: // AnnotatedDateTime[?Zoned] auto dateTime = annotatedDateTime(); @@ -3209,10 +3205,10 @@ mozilla::Result TemporalParser::parseTemporalZonedDateTimeString() { // Parse goal: TemporalDateTimeString[+Zoned] // - // TemporalDateTimeString[Zoned] : + // TemporalDateTimeString[Zoned] ::: // AnnotatedDateTime[?Zoned] // - // AnnotatedDateTime[Zoned] : + // AnnotatedDateTime[Zoned] ::: // [~Zoned] DateTime TimeZoneAnnotation? Annotations? // [+Zoned] DateTime TimeZoneAnnotation Annotations? @@ -3269,7 +3265,7 @@ static auto ParseTemporalZonedDateTimeString(Handle str) { bool js::temporal::ParseTemporalZonedDateTimeString( JSContext* cx, Handle str, PlainDateTime* dateTime, bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, - MutableHandle timeZoneName, + MutableHandle timeZoneAnnotation, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { @@ -3313,7 +3309,7 @@ bool js::temporal::ParseTemporalZonedDateTimeString( // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } const auto& annotation = parsed.timeZone.annotation; - if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneName)) { + if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneAnnotation)) { return false; } @@ -3370,7 +3366,7 @@ static auto ParseTemporalRelativeToString(Handle str) { bool js::temporal::ParseTemporalRelativeToString( JSContext* cx, Handle str, PlainDateTime* dateTime, bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, - MutableHandle timeZoneName, + MutableHandle timeZoneAnnotation, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { @@ -3420,7 +3416,7 @@ bool js::temporal::ParseTemporalRelativeToString( // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } const auto& annotation = parsed.timeZone.annotation; - if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneName)) { + if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneAnnotation)) { return false; } @@ -3444,7 +3440,7 @@ bool js::temporal::ParseTemporalRelativeToString( *isUTC = false; *hasOffset = false; *timeZoneOffset = 0; - timeZoneName.set(ParsedTimeZone{}); + timeZoneAnnotation.set(ParsedTimeZone{}); } // Step 4. (ParseISODateTime, steps 23-24.) diff --git a/js/src/builtin/temporal/TemporalParser.h b/js/src/builtin/temporal/TemporalParser.h index 677a90b58d..86ac7bbd82 100644 --- a/js/src/builtin/temporal/TemporalParser.h +++ b/js/src/builtin/temporal/TemporalParser.h @@ -8,6 +8,7 @@ #define builtin_temporal_TemporalParser_h #include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" #include #include @@ -26,7 +27,7 @@ struct PlainDate; struct PlainDateTime; struct PlainTime; -struct ParsedTimeZone { +struct MOZ_STACK_CLASS ParsedTimeZone final { JSLinearString* name = nullptr; int32_t offset = INT32_MIN; @@ -129,7 +130,7 @@ bool ParseTemporalDateTimeString(JSContext* cx, JS::Handle str, bool ParseTemporalZonedDateTimeString( JSContext* cx, JS::Handle str, PlainDateTime* dateTime, bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, - JS::MutableHandle timeZoneName, + JS::MutableHandle timeZoneAnnotation, JS::MutableHandle calendar); /** @@ -138,7 +139,7 @@ bool ParseTemporalZonedDateTimeString( bool ParseTemporalRelativeToString( JSContext* cx, JS::Handle str, PlainDateTime* dateTime, bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, - JS::MutableHandle timeZoneName, + JS::MutableHandle timeZoneAnnotation, JS::MutableHandle calendar); } /* namespace js::temporal */ diff --git a/js/src/builtin/temporal/TemporalRoundingMode.h b/js/src/builtin/temporal/TemporalRoundingMode.h index 91ef758fc6..23d3996d09 100644 --- a/js/src/builtin/temporal/TemporalRoundingMode.h +++ b/js/src/builtin/temporal/TemporalRoundingMode.h @@ -12,6 +12,8 @@ #include #include +#include "builtin/temporal/Int128.h" + namespace js::temporal { // Overview of integer rounding modes is available at @@ -428,6 +430,286 @@ inline int64_t Divide(int64_t dividend, int64_t divisor, MOZ_CRASH("invalid rounding mode"); } +/** + * Compute ceiling division ⌈dividend / divisor⌉. The divisor must be a positive + * number. + */ +constexpr Int128 CeilDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // Ceiling division rounds the quotient toward positive infinity. When the + // quotient is negative, this is equivalent to rounding toward zero. See [1]. + // + // Int128 division truncates, so rounding toward zero for negative quotients + // is already covered. When there is a non-zero positive remainder, the + // quotient is positive and we have to increment it by one to implement + // rounding toward positive infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder > Int128{0}) { + quotient += Int128{1}; + } + return quotient; +} + +/** + * Compute floor division ⌊dividend / divisor⌋. The divisor must be a positive + * number. + */ +constexpr Int128 FloorDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // Floor division rounds the quotient toward negative infinity. When the + // quotient is positive, this is equivalent to rounding toward zero. See [1]. + // + // Int128 division truncates, so rounding toward zero for positive quotients + // is already covered. When there is a non-zero negative remainder, the + // quotient is negative and we have to decrement it by one to implement + // rounding toward negative infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder < Int128{0}) { + quotient -= Int128{1}; + } + return quotient; +} + +/** + * Compute "round toward infinity" division `dividend / divisor`. The divisor + * must be a positive number. + */ +constexpr Int128 ExpandDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // "Round toward infinity" division rounds positive quotients toward positive + // infinity and negative quotients toward negative infinity. See [1]. + // + // When there is a non-zero positive remainder, the quotient is positive and + // we have to increment it by one to implement rounding toward positive + // infinity. When there is a non-zero negative remainder, the quotient is + // negative and we have to decrement it by one to implement rounding toward + // negative infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder > Int128{0}) { + quotient += Int128{1}; + } + if (remainder < Int128{0}) { + quotient -= Int128{1}; + } + return quotient; +} + +/** + * Compute truncating division `dividend / divisor`. The divisor must be a + * positive number. + */ +constexpr Int128 TruncDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + // Truncating division rounds both positive and negative quotients toward + // zero, cf. [1]. + // + // Int128 division truncates, so rounding toward zero implicitly happens. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + return dividend / divisor; +} + +/** + * Compute "round half toward positive infinity" division `dividend / divisor`. + * The divisor must be a positive number. + */ +inline Int128 HalfCeilDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // "Round half toward positive infinity" division rounds the quotient toward + // positive infinity when the fractional part of the remainder is ≥0.5. When + // the quotient is negative, this is equivalent to rounding toward zero + // instead of toward positive infinity. See [1]. + // + // When the remainder is a non-zero positive value, the quotient is positive, + // too. When additionally the fractional part of the remainder is ≥0.5, we + // have to increment the quotient by one to implement rounding toward positive + // infinity. + // + // Int128 division truncates, so we implicitly round toward zero for negative + // quotients. When the absolute value of the fractional part of the remainder + // is >0.5, we should instead have rounded toward negative infinity, so we + // need to decrement the quotient by one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder > Int128{0} && + Uint128(remainder.abs()) * Uint128{2} >= static_cast(divisor)) { + quotient += Int128{1}; + } + if (remainder < Int128{0} && + Uint128(remainder.abs()) * Uint128{2} > static_cast(divisor)) { + quotient -= Int128{1}; + } + return quotient; +} + +/** + * Compute "round half toward negative infinity" division `dividend / divisor`. + * The divisor must be a positive number. + */ +inline Int128 HalfFloorDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // "Round half toward negative infinity" division rounds the quotient toward + // negative infinity when the fractional part of the remainder is ≥0.5. When + // the quotient is positive, this is equivalent to rounding toward zero + // instead of toward negative infinity. See [1]. + // + // When the remainder is a non-zero negative value, the quotient is negative, + // too. When additionally the fractional part of the remainder is ≥0.5, we + // have to decrement the quotient by one to implement rounding toward negative + // infinity. + // + // Int128 division truncates, so we implicitly round toward zero for positive + // quotients. When the absolute value of the fractional part of the remainder + // is >0.5, we should instead have rounded toward positive infinity, so we + // need to increment the quotient by one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder < Int128{0} && + Uint128(remainder.abs()) * Uint128{2} >= static_cast(divisor)) { + quotient -= Int128{1}; + } + if (remainder > Int128{0} && + Uint128(remainder.abs()) * Uint128{2} > static_cast(divisor)) { + quotient += Int128{1}; + } + return quotient; +} + +/** + * Compute "round half toward infinity" division `dividend / divisor`. The + * divisor must be a positive number. + */ +inline Int128 HalfExpandDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // "Round half toward infinity" division rounds positive quotients whose + // remainder has a fractional part ≥0.5 toward positive infinity. And negative + // quotients whose remainder has a fractional part ≥0.5 toward negative + // infinity. See [1]. + // + // Int128 division truncates, which means it rounds toward zero, so we have + // to increment resp. decrement the quotient when the fractional part of the + // remainder is ≥0.5 to round toward ±infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (Uint128(remainder.abs()) * Uint128{2} >= static_cast(divisor)) { + quotient += (dividend > Int128{0}) ? Int128{1} : Int128{-1}; + } + return quotient; +} + +/** + * Compute "round half toward zero" division `dividend / divisor`. The divisor + * must be a positive number. + */ +inline Int128 HalfTruncDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // "Round half toward zero" division rounds both positive and negative + // quotients whose remainder has a fractional part ≤0.5 toward zero. See [1]. + // + // Int128 division truncates, so we implicitly round toward zero. When the + // fractional part of the remainder is >0.5, we should instead have rounded + // toward ±infinity, so we need to increment resp. decrement the quotient by + // one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (Uint128(remainder.abs()) * Uint128{2} > static_cast(divisor)) { + quotient += (dividend > Int128{0}) ? Int128{1} : Int128{-1}; + } + return quotient; +} + +/** + * Compute "round half to even" division `dividend / divisor`. The divisor must + * be a positive number. + */ +inline Int128 HalfEvenDiv(const Int128& dividend, const Int128& divisor) { + MOZ_ASSERT(divisor > Int128{0}, "negative divisor not supported"); + + auto [quotient, remainder] = dividend.divrem(divisor); + + // "Round half to even" division rounds both positive and negative quotients + // to the nearest even integer. See [1]. + // + // Int128 division truncates, so we implicitly round toward zero. When the + // fractional part of the remainder is 0.5 and the quotient is odd or when the + // fractional part of the remainder is >0.5, we should instead have rounded + // toward ±infinity, so we need to increment resp. decrement the quotient by + // one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if ((quotient & Int128{1}) == Int128{1} && + Uint128(remainder.abs()) * Uint128{2} == static_cast(divisor)) { + quotient += (dividend > Int128{0}) ? Int128{1} : Int128{-1}; + } + if (Uint128(remainder.abs()) * Uint128{2} > static_cast(divisor)) { + quotient += (dividend > Int128{0}) ? Int128{1} : Int128{-1}; + } + return quotient; +} + +/** + * Perform `dividend / divisor` and round the result according to the given + * rounding mode. + */ +inline Int128 Divide(const Int128& dividend, const Int128& divisor, + TemporalRoundingMode roundingMode) { + switch (roundingMode) { + case TemporalRoundingMode::Ceil: + return CeilDiv(dividend, divisor); + case TemporalRoundingMode::Floor: + return FloorDiv(dividend, divisor); + case TemporalRoundingMode::Expand: + return ExpandDiv(dividend, divisor); + case TemporalRoundingMode::Trunc: + return TruncDiv(dividend, divisor); + case TemporalRoundingMode::HalfCeil: + return HalfCeilDiv(dividend, divisor); + case TemporalRoundingMode::HalfFloor: + return HalfFloorDiv(dividend, divisor); + case TemporalRoundingMode::HalfExpand: + return HalfExpandDiv(dividend, divisor); + case TemporalRoundingMode::HalfTrunc: + return HalfTruncDiv(dividend, divisor); + case TemporalRoundingMode::HalfEven: + return HalfEvenDiv(dividend, divisor); + } + MOZ_CRASH("invalid rounding mode"); +} + } /* namespace js::temporal */ #endif /* builtin_temporal_TemporalRoundingMode_h */ diff --git a/js/src/builtin/temporal/TemporalTypes.h b/js/src/builtin/temporal/TemporalTypes.h index 654fd65a3b..b645a9a175 100644 --- a/js/src/builtin/temporal/TemporalTypes.h +++ b/js/src/builtin/temporal/TemporalTypes.h @@ -11,7 +11,9 @@ #include "mozilla/CheckedInt.h" #include +#include +#include "builtin/temporal/Int128.h" #include "builtin/temporal/TemporalUnit.h" namespace js::temporal { @@ -181,15 +183,19 @@ struct SecondsAndNanoseconds { /** * Return the nanoseconds value. - * - * The returned nanoseconds amount can be invalid on overflow. The caller is - * responsible for handling the overflow case. */ - constexpr mozilla::CheckedInt64 toNanoseconds() const { - mozilla::CheckedInt64 nanos = seconds; - nanos *= ToNanoseconds(TemporalUnit::Second); - nanos += nanoseconds; - return nanos; + constexpr Int128 toNanoseconds() const { + return Int128{seconds} * Int128{ToNanoseconds(TemporalUnit::Second)} + + Int128{nanoseconds}; + } + + /** + * Cast to a different representation. + */ + template + constexpr Other to() const { + static_assert(std::is_base_of_v, Other>); + return Other{seconds, nanoseconds}; } /** @@ -209,7 +215,7 @@ struct SecondsAndNanoseconds { */ static constexpr Derived fromMilliseconds(int64_t milliseconds) { int64_t seconds = milliseconds / 1'000; - int32_t millis = milliseconds % 1'000; + int32_t millis = int32_t(milliseconds % 1'000); if (millis < 0) { seconds -= 1; millis += 1'000; @@ -222,7 +228,7 @@ struct SecondsAndNanoseconds { */ static constexpr Derived fromMicroseconds(int64_t microseconds) { int64_t seconds = microseconds / 1'000'000; - int32_t micros = microseconds % 1'000'000; + int32_t micros = int32_t(microseconds % 1'000'000); if (micros < 0) { seconds -= 1; micros += 1'000'000; @@ -235,7 +241,21 @@ struct SecondsAndNanoseconds { */ static constexpr Derived fromNanoseconds(int64_t nanoseconds) { int64_t seconds = nanoseconds / 1'000'000'000; - int32_t nanos = nanoseconds % 1'000'000'000; + int32_t nanos = int32_t(nanoseconds % 1'000'000'000); + if (nanos < 0) { + seconds -= 1; + nanos += 1'000'000'000; + } + return {seconds, nanos}; + } + + /** + * Create from a nanoseconds value. + */ + static constexpr Derived fromNanoseconds(const Int128& nanoseconds) { + auto div = nanoseconds.divrem(Int128{1'000'000'000}); + int64_t seconds = int64_t(div.first); + int32_t nanos = int32_t(div.second); if (nanos < 0) { seconds -= 1; nanos += 1'000'000'000; @@ -365,6 +385,32 @@ struct Instant final : SecondsAndNanoseconds { static constexpr Instant min() { return -max(); } }; +// Minimum and maximum valid epoch day relative to midnight at the beginning of +// 1 January 1970 UTC. +// +// NOTE in ISODateTimeWithinLimits: +// +// Temporal.PlainDateTime objects can represent points in time within 24 hours +// (8.64 × 10**13 nanoseconds) of the Temporal.Instant boundaries. This ensures +// that a Temporal.Instant object can be converted into a Temporal.PlainDateTime +// object using any time zone. +// +// This limits the maximum valid date-time to +275760-09-13T23:59:59.999Z and +// the minimum valid date-time -271821-04-19T00:00:00.001Z. The corresponding +// maximum and minimum valid date values are therefore +275760-09-13 and +// -271821-04-19. There are exactly 100'000'000 days from 1 January 1970 UTC to +// the maximum valid date and -100'000'001 days to the minimum valid date. +constexpr inline int32_t MinEpochDay = -100'000'001; +constexpr inline int32_t MaxEpochDay = 100'000'000; + +static_assert(MinEpochDay == + Instant::min().seconds / ToSeconds(TemporalUnit::Day) - 1); +static_assert(MaxEpochDay == + Instant::max().seconds / ToSeconds(TemporalUnit::Day)); + +// Maximum number of days between two valid epoch days. +constexpr inline int32_t MaxEpochDaysDuration = MaxEpochDay - MinEpochDay; + /** * Plain date represents a date in the ISO 8601 calendar. */ @@ -437,6 +483,9 @@ struct PlainDateTime final { } }; +struct DateDuration; +struct TimeDuration; + /** * Duration represents the difference between dates or times. Each duration * component is an integer and all components must have the same sign. @@ -453,7 +502,7 @@ struct Duration final { double microseconds = 0; double nanoseconds = 0; - bool operator==(const Duration& other) const { + constexpr bool operator==(const Duration& other) const { return years == other.years && months == other.months && weeks == other.weeks && days == other.days && hours == other.hours && minutes == other.minutes && seconds == other.seconds && @@ -462,35 +511,14 @@ struct Duration final { nanoseconds == other.nanoseconds; } - bool operator!=(const Duration& other) const { return !(*this == other); } - - /** - * Return the date components of this duration. - */ - Duration date() const { return {years, months, weeks, days}; } - - /** - * Return the time components of this duration. - */ - Duration time() const { - return { - 0, - 0, - 0, - 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - }; + constexpr bool operator!=(const Duration& other) const { + return !(*this == other); } /** * Return a new duration with every component negated. */ - Duration negate() const { + constexpr Duration negate() const { // Add zero to convert -0 to +0. return { -years + (+0.0), -months + (+0.0), -weeks + (+0.0), @@ -499,6 +527,11 @@ struct Duration final { -nanoseconds + (+0.0), }; } + + /** + * Return the date components of this duration. + */ + inline DateDuration toDateDuration() const; }; /** @@ -506,41 +539,144 @@ struct Duration final { * component is an integer and all components must have the same sign. */ struct DateDuration final { - double years = 0; - double months = 0; - double weeks = 0; - double days = 0; + // abs(years) < 2**32 + int64_t years = 0; + + // abs(months) < 2**32 + int64_t months = 0; + + // abs(weeks) < 2**32 + int64_t weeks = 0; + + // abs(days) < ⌈(2**53) / (24 * 60 * 60)⌉ + int64_t days = 0; - Duration toDuration() { return {years, months, weeks, days}; } + constexpr bool operator==(const DateDuration& other) const { + return years == other.years && months == other.months && + weeks == other.weeks && days == other.days; + } + + constexpr bool operator!=(const DateDuration& other) const { + return !(*this == other); + } + + constexpr Duration toDuration() const { + return { + double(years), + double(months), + double(weeks), + double(days), + }; + } }; +inline DateDuration Duration::toDateDuration() const { + return {int64_t(years), int64_t(months), int64_t(weeks), int64_t(days)}; +} + /** * Time duration represents the difference between times. Each duration * component is an integer and all components must have the same sign. */ struct TimeDuration final { - double days = 0; - double hours = 0; - double minutes = 0; - double seconds = 0; - double milliseconds = 0; + // abs(days) < ⌈(2**53) / (24 * 60 * 60)⌉ + int64_t days = 0; + + // abs(hours) < ⌈(2**53) / (60 * 60)⌉ + int64_t hours = 0; + + // abs(minutes) < ⌈(2**53) / 60⌉ + int64_t minutes = 0; + + // abs(seconds) < (2**53) + int64_t seconds = 0; + + // abs(milliseconds) < (2**53) * (1000**1) + int64_t milliseconds = 0; + + // abs(microseconds) < (2**53) * (1000**2) double microseconds = 0; + + // abs(nanoseconds) < (2**53) * (1000**3) double nanoseconds = 0; - Duration toDuration() { + constexpr Duration toDuration() const { return {0, 0, 0, - days, - hours, - minutes, - seconds, - milliseconds, + double(days), + double(hours), + double(minutes), + double(seconds), + double(milliseconds), microseconds, nanoseconds}; } }; +/** + * Normalized time duration with a seconds value in the range + * [-9'007'199'254'740'991, +9'007'199'254'740'991] and a nanoseconds value in + * the range [0, 999'999'999]. + */ +struct NormalizedTimeDuration final + : SecondsAndNanoseconds { + constexpr NormalizedTimeDuration& operator+=( + const NormalizedTimeDuration& other) { + *this = add(*this, other); + return *this; + } + + constexpr NormalizedTimeDuration& operator-=( + const NormalizedTimeDuration& other) { + *this = subtract(*this, other); + return *this; + } + + constexpr NormalizedTimeDuration operator+( + const NormalizedTimeDuration& other) const { + return add(*this, other); + } + + constexpr NormalizedTimeDuration operator-( + const NormalizedTimeDuration& other) const { + return subtract(*this, other); + } + + constexpr NormalizedTimeDuration operator-() const { return negate(*this); } + + /** + * Returns the maximum normalized time duration value. + */ + static constexpr NormalizedTimeDuration max() { + constexpr int64_t seconds = 0x1f'ffff'ffff'ffff; + constexpr int64_t nanos = 999'999'999; + return {seconds, nanos}; + } + + /** + * Returns the minimum normalized time duration value. + */ + static constexpr NormalizedTimeDuration min() { return -max(); } +}; + +/** + * Duration represents the difference between dates or times. Each duration + * component is an integer and all components must have the same sign. + */ +struct NormalizedDuration final { + DateDuration date; + NormalizedTimeDuration time; + + constexpr bool operator==(const NormalizedDuration& other) const { + return date == other.date && time == other.time; + } + + constexpr bool operator!=(const NormalizedDuration& other) const { + return !(*this == other); + } +}; + } /* namespace js::temporal */ #endif /* builtin_temporal_TemporalTypes_h */ diff --git a/js/src/builtin/temporal/TemporalUnit.h b/js/src/builtin/temporal/TemporalUnit.h index 3c8801cb85..0294c1062d 100644 --- a/js/src/builtin/temporal/TemporalUnit.h +++ b/js/src/builtin/temporal/TemporalUnit.h @@ -52,6 +52,31 @@ constexpr int64_t ToNanoseconds(TemporalUnit unit) { MOZ_CRASH("Unexpected temporal unit"); } +constexpr int64_t ToMicroseconds(TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Day: + return 86'400'000'000; + case TemporalUnit::Hour: + return 3'600'000'000; + case TemporalUnit::Minute: + return 60'000'000; + case TemporalUnit::Second: + return 1'000'000; + case TemporalUnit::Millisecond: + return 1'000; + case TemporalUnit::Microsecond: + return 1; + + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Nanosecond: + break; + } + MOZ_CRASH("Unexpected temporal unit"); +} + constexpr int64_t ToMilliseconds(TemporalUnit unit) { switch (unit) { case TemporalUnit::Day: @@ -76,6 +101,29 @@ constexpr int64_t ToMilliseconds(TemporalUnit unit) { MOZ_CRASH("Unexpected temporal unit"); } +constexpr int64_t ToSeconds(TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Day: + return 86'400; + case TemporalUnit::Hour: + return 3'600; + case TemporalUnit::Minute: + return 60; + case TemporalUnit::Second: + return 1; + + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Millisecond: + case TemporalUnit::Microsecond: + case TemporalUnit::Nanosecond: + break; + } + MOZ_CRASH("Unexpected temporal unit"); +} + constexpr int64_t UnitsPerDay(TemporalUnit unit) { switch (unit) { case TemporalUnit::Day: diff --git a/js/src/builtin/temporal/TimeZone.cpp b/js/src/builtin/temporal/TimeZone.cpp index ca7e1b9f11..8282b6739e 100644 --- a/js/src/builtin/temporal/TimeZone.cpp +++ b/js/src/builtin/temporal/TimeZone.cpp @@ -1131,23 +1131,23 @@ JSString* js::temporal::FormatUTCOffsetNanoseconds(JSContext* cx, // Steps 7-8. (Inlined FormatTimeString). result[n++] = sign; - result[n++] = '0' + (hour / 10); - result[n++] = '0' + (hour % 10); + result[n++] = char('0' + (hour / 10)); + result[n++] = char('0' + (hour % 10)); result[n++] = ':'; - result[n++] = '0' + (minute / 10); - result[n++] = '0' + (minute % 10); + result[n++] = char('0' + (minute / 10)); + result[n++] = char('0' + (minute % 10)); if (second != 0 || subSecondNanoseconds != 0) { result[n++] = ':'; - result[n++] = '0' + (second / 10); - result[n++] = '0' + (second % 10); + result[n++] = char('0' + (second / 10)); + result[n++] = char('0' + (second % 10)); if (uint32_t fractional = subSecondNanoseconds) { result[n++] = '.'; uint32_t k = 100'000'000; do { - result[n++] = '0' + (fractional / k); + result[n++] = char('0' + (fractional / k)); fractional %= k; k /= 10; } while (fractional); @@ -1338,28 +1338,28 @@ static PlainDateTime GetISOPartsFromEpoch(const Instant& instant) { int32_t remainderNs = instant.nanoseconds % 1'000'000; // Step 3. - int64_t epochMilliseconds = instant.floorToMilliseconds(); + double epochMilliseconds = double(instant.floorToMilliseconds()); // Step 4. - int32_t year = JS::YearFromTime(epochMilliseconds); + int32_t year = int32_t(JS::YearFromTime(epochMilliseconds)); // Step 5. - int32_t month = JS::MonthFromTime(epochMilliseconds) + 1; + int32_t month = int32_t(JS::MonthFromTime(epochMilliseconds)) + 1; // Step 6. - int32_t day = JS::DayFromTime(epochMilliseconds); + int32_t day = int32_t(JS::DayFromTime(epochMilliseconds)); // Step 7. - int32_t hour = HourFromTime(epochMilliseconds); + int32_t hour = int32_t(HourFromTime(epochMilliseconds)); // Step 8. - int32_t minute = MinFromTime(epochMilliseconds); + int32_t minute = int32_t(MinFromTime(epochMilliseconds)); // Step 9. - int32_t second = SecFromTime(epochMilliseconds); + int32_t second = int32_t(SecFromTime(epochMilliseconds)); // Step 10. - int32_t millisecond = msFromTime(epochMilliseconds); + int32_t millisecond = int32_t(msFromTime(epochMilliseconds)); // Step 11. int32_t microsecond = remainderNs / 1000; @@ -1389,11 +1389,11 @@ static PlainDateTime BalanceISODateTime(const PlainDateTime& dateTime, MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); MOZ_ASSERT(std::abs(nanoseconds) < ToNanoseconds(TemporalUnit::Day)); - auto& [date, time] = dateTime; + const auto& [date, time] = dateTime; // Step 1. auto balancedTime = BalanceTime(time, nanoseconds); - MOZ_ASSERT(-1 <= balancedTime.days && balancedTime.days <= 1); + MOZ_ASSERT(std::abs(balancedTime.days) <= 1); // Step 2. auto balancedDate = @@ -1429,9 +1429,6 @@ static PlainDateTimeObject* GetPlainDateTimeFor( // Steps 5-7. auto dateTime = GetPlainDateTimeFor(ToInstant(unwrappedInstant), offsetNanoseconds); - - // FIXME: spec issue - CreateTemporalDateTime is infallible - // https://github.com/tc39/proposal-temporal/issues/2523 MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); return CreateTemporalDateTime(cx, dateTime, calendar); @@ -1455,9 +1452,6 @@ PlainDateTime js::temporal::GetPlainDateTimeFor(const Instant& instant, // Step 6. auto balanced = BalanceISODateTime(dateTime, offsetNanoseconds); - - // FIXME: spec issue - CreateTemporalDateTime is infallible - // https://github.com/tc39/proposal-temporal/issues/2523 MOZ_ASSERT(ISODateTimeWithinLimits(balanced)); // Step 7. @@ -1524,9 +1518,6 @@ PlainDateTimeObject* js::temporal::GetPlainDateTimeFor( if (!GetPlainDateTimeFor(cx, timeZone, instant, &dateTime)) { return nullptr; } - - // FIXME: spec issue - CreateTemporalDateTime is infallible - // https://github.com/tc39/proposal-temporal/issues/2523 MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); // Step 7. @@ -1544,9 +1535,6 @@ PlainDateTimeObject* js::temporal::GetPlainDateTimeFor( // Steps 1-6. auto dateTime = GetPlainDateTimeFor(instant, offsetNanoseconds); - - // FIXME: spec issue - CreateTemporalDateTime is infallible - // https://github.com/tc39/proposal-temporal/issues/2523 MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); // Step 7. @@ -1628,6 +1616,8 @@ static bool GetPossibleInstantsForSlow( JSContext* cx, Handle timeZone, Handle> dateTime, MutableHandle list) { + MOZ_ASSERT(list.empty()); + // Step 1. (Inlined call to TimeZoneMethodsRecordCall) Rooted fval(cx, ObjectValue(*timeZone.getPossibleInstantsFor())); auto thisv = timeZone.receiver().toObject(); @@ -1647,23 +1637,46 @@ static bool GetPossibleInstantsForSlow( // Step 4. (Not applicable in our implementation.) - // Steps 5-6. + // Step 5. + auto min = Instant::max(); + auto max = Instant::min(); Rooted nextValue(cx); while (true) { - // Steps 6.a and 6.b.i. + // Step 5.a. bool done; if (!iterator.next(&nextValue, &done)) { return false; } + + // Step 5.b. if (done) { - break; + // Steps 5.b.i-ii. + if (list.length() > 1) { + // Steps 5.b.ii.1-4. (Not applicable in our implementation.) + + // Step 5.b.ii.5. + constexpr auto nsPerDay = + InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day)); + if ((max - min).abs() > nsPerDay) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_OFFSET_SHIFT_ONE_DAY); + return false; + } + } + + // Step 5.b.iii. + return true; } - // Steps 6.b.ii. + // Step 5.d. (Reordered) if (nextValue.isObject()) { JSObject* obj = &nextValue.toObject(); - if (obj->canUnwrapAs()) { - // Step 6.b.iii. + if (auto* unwrapped = obj->maybeUnwrapIf()) { + auto instant = ToInstant(unwrapped); + min = std::min(min, instant); + max = std::max(max, instant); + if (!list.append(obj)) { return false; } @@ -1671,17 +1684,14 @@ static bool GetPossibleInstantsForSlow( } } - // Step 6.b.ii.1. + // Step 5.c.1. ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, nullptr, "not an instant"); - // Step 6.b.ii.2. + // Step 5.c.2. iterator.closeThrow(); return false; } - - // Step 7. - return true; } /** @@ -1714,7 +1724,7 @@ static bool GetPossibleInstantsFor( } } - // Steps 1 and 3-7. + // Steps 1 and 3-5. return GetPossibleInstantsForSlow(cx, timeZone, dateTimeObj, list); } @@ -1753,7 +1763,7 @@ bool js::temporal::GetPossibleInstantsFor( return false; } - // Steps 1 and 3-7. + // Steps 1 and 3-5. return GetPossibleInstantsForSlow(cx, timeZone, dateTimeObj, list); } @@ -1763,9 +1773,9 @@ bool js::temporal::GetPossibleInstantsFor( */ static auto AddTime(const PlainTime& time, int64_t nanoseconds) { MOZ_ASSERT(IsValidTime(time)); - MOZ_ASSERT(std::abs(nanoseconds) <= 2 * ToNanoseconds(TemporalUnit::Day)); + MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day)); - // Steps 1-7. + // Steps 1-3. return BalanceTime(time, nanoseconds); } @@ -1871,79 +1881,80 @@ bool js::temporal::DisambiguatePossibleInstants( int64_t nanoseconds = offsetAfter - offsetBefore; // Step 18. + if (std::abs(nanoseconds) > ToNanoseconds(TemporalUnit::Day)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_OFFSET_SHIFT_ONE_DAY); + return false; + } + + // Step 19. if (disambiguation == TemporalDisambiguation::Earlier) { - // Step 18.a. + // Steps 19.a-b. auto earlierTime = ::AddTime(dateTime.time, -nanoseconds); - MOZ_ASSERT(std::abs(earlierTime.days) <= 2, - "subtracting nanoseconds is at most two days"); + MOZ_ASSERT(std::abs(earlierTime.days) <= 1, + "subtracting nanoseconds is at most one day"); - // Step 18.b. - PlainDate earlierDate; - if (!AddISODate(cx, dateTime.date, {0, 0, 0, double(earlierTime.days)}, - TemporalOverflow::Constrain, &earlierDate)) { - return false; - } + // Step 19.c. + auto earlierDate = BalanceISODate(dateTime.date.year, dateTime.date.month, + dateTime.date.day + earlierTime.days); - // Step 18.c. + // Step 19.d. Rooted calendar(cx, CalendarValue(cx->names().iso8601)); Rooted earlierDateTime( cx, PlainDateTimeWithCalendar{{earlierDate, earlierTime.time}, calendar}); - // Step 18.d. + // Step 19.e. Rooted earlierInstants(cx, InstantVector(cx)); if (!GetPossibleInstantsFor(cx, timeZone, earlierDateTime, &earlierInstants)) { return false; } - // Step 18.e. + // Step 19.f. if (earlierInstants.empty()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); return false; } - // Step 18.f. + // Step 19.g. result.set(earlierInstants[0]); return true; } - // Step 19. + // Step 20. MOZ_ASSERT(disambiguation == TemporalDisambiguation::Compatible || disambiguation == TemporalDisambiguation::Later); - // Step 20. + // Steps 21-22. auto laterTime = ::AddTime(dateTime.time, nanoseconds); - MOZ_ASSERT(std::abs(laterTime.days) <= 2, - "adding nanoseconds is at most two days"); + MOZ_ASSERT(std::abs(laterTime.days) <= 1, + "adding nanoseconds is at most one day"); - // Step 21. - PlainDate laterDate; - if (!AddISODate(cx, dateTime.date, {0, 0, 0, double(laterTime.days)}, - TemporalOverflow::Constrain, &laterDate)) { - return false; - } + // Step 23. + auto laterDate = BalanceISODate(dateTime.date.year, dateTime.date.month, + dateTime.date.day + laterTime.days); - // Step 22. + // Step 24. Rooted calendar(cx, CalendarValue(cx->names().iso8601)); Rooted laterDateTime( cx, PlainDateTimeWithCalendar{{laterDate, laterTime.time}, calendar}); - // Step 23. + // Step 25. Rooted laterInstants(cx, InstantVector(cx)); if (!GetPossibleInstantsFor(cx, timeZone, laterDateTime, &laterInstants)) { return false; } - // Steps 24-25. + // Steps 26-27. if (laterInstants.empty()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); return false; } - // Step 26. + // Step 28. size_t last = laterInstants.length() - 1; result.set(laterInstants[last]); return true; @@ -2282,11 +2293,14 @@ static bool TimeZone_getOffsetNanosecondsFor(JSContext* cx, unsigned argc, static bool TimeZone_getOffsetStringFor(JSContext* cx, const CallArgs& args) { Rooted timeZone(cx, &args.thisv().toObject()); - // FIXME: spec issue - CreateTimeZoneMethodsRecord called before - // ToTemporalInstant whereas TimeZone.p.{getPlainDateTimeFor,getInstantFor} - // first convert the input arguments. - // Step 3. + Rooted> instant(cx, + ToTemporalInstant(cx, args.get(0))); + if (!instant) { + return false; + } + + // Step 4. Rooted timeZoneRec(cx); if (!CreateTimeZoneMethodsRecord(cx, timeZone, { @@ -2296,13 +2310,6 @@ static bool TimeZone_getOffsetStringFor(JSContext* cx, const CallArgs& args) { return false; } - // Step 4. - Rooted> instant(cx, - ToTemporalInstant(cx, args.get(0))); - if (!instant) { - return false; - } - // Step 5. JSString* str = GetOffsetStringFor(cx, timeZoneRec, instant); if (!str) { diff --git a/js/src/builtin/temporal/TimeZone.h b/js/src/builtin/temporal/TimeZone.h index f1d0bf3f1f..f13421111a 100644 --- a/js/src/builtin/temporal/TimeZone.h +++ b/js/src/builtin/temporal/TimeZone.h @@ -8,6 +8,7 @@ #define builtin_temporal_TimeZone_h #include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" #include "mozilla/EnumSet.h" #include @@ -142,7 +143,7 @@ namespace js::temporal { * * Option 2 is a bit easier to implement, so we use this approach for now. */ -class TimeZoneValue final { +class MOZ_STACK_CLASS TimeZoneValue final { JSObject* object_ = nullptr; public: @@ -252,7 +253,7 @@ enum class TimeZoneMethod { GetPossibleInstantsFor, }; -class TimeZoneRecord { +class MOZ_STACK_CLASS TimeZoneRecord final { TimeZoneValue receiver_; // Null unless non-builtin time zone methods are used. diff --git a/js/src/builtin/temporal/ToString.cpp b/js/src/builtin/temporal/ToString.cpp index c789c5e95c..e13ad8eb03 100644 --- a/js/src/builtin/temporal/ToString.cpp +++ b/js/src/builtin/temporal/ToString.cpp @@ -169,7 +169,7 @@ static void FormatFractionalSeconds(TemporalStringBuilder& result, result.append('.'); // Steps 1.b-c. - uint32_t k = 100'000'000; + int32_t k = 100'000'000; do { result.append(char('0' + (subSecondNanoseconds / k))); subSecondNanoseconds %= k; @@ -186,7 +186,7 @@ static void FormatFractionalSeconds(TemporalStringBuilder& result, result.append('.'); // Steps 2.b-c. - uint32_t k = 100'000'000; + int32_t k = 100'000'000; for (uint8_t i = 0; i < p; i++) { result.append(char('0' + (subSecondNanoseconds / k))); subSecondNanoseconds %= k; @@ -274,7 +274,7 @@ static int32_t RoundNanosecondsToMinutes(int64_t offsetNanoseconds) { if (std::abs(remainder * 2) >= increment) { quotient += (offsetNanoseconds > 0 ? 1 : -1); } - return quotient; + return int32_t(quotient); } /** @@ -636,11 +636,8 @@ JSString* js::temporal::TemporalZonedDateTimeToString( // Steps 1-3. (Not applicable in our implementation.) // Step 4. - Instant ns; - if (!RoundTemporalInstant(cx, zonedDateTime.instant(), increment, unit, - roundingMode, &ns)) { - return nullptr; - } + auto ns = RoundTemporalInstant(zonedDateTime.instant(), increment, unit, + roundingMode); // Step 5. auto timeZone = zonedDateTime.timeZone(); diff --git a/js/src/builtin/temporal/ZonedDateTime.cpp b/js/src/builtin/temporal/ZonedDateTime.cpp index 92842a9626..e75b368ba9 100644 --- a/js/src/builtin/temporal/ZonedDateTime.cpp +++ b/js/src/builtin/temporal/ZonedDateTime.cpp @@ -10,6 +10,7 @@ #include "mozilla/Maybe.h" #include +#include #include #include "jspubtd.h" @@ -18,6 +19,7 @@ #include "builtin/temporal/Calendar.h" #include "builtin/temporal/Duration.h" #include "builtin/temporal/Instant.h" +#include "builtin/temporal/Int96.h" #include "builtin/temporal/PlainDate.h" #include "builtin/temporal/PlainDateTime.h" #include "builtin/temporal/PlainMonthDay.h" @@ -119,7 +121,8 @@ bool js::temporal::InterpretISODateTimeOffset( // Step 5. if (offsetBehaviour == OffsetBehaviour::Wall || - offsetOption == TemporalOffset::Ignore) { + (offsetBehaviour == OffsetBehaviour::Option && + offsetOption == TemporalOffset::Ignore)) { // Steps 5.a-b. return GetInstantFor(cx, timeZone, temporalDateTime, disambiguation, result); @@ -127,7 +130,8 @@ bool js::temporal::InterpretISODateTimeOffset( // Step 6. if (offsetBehaviour == OffsetBehaviour::Exact || - offsetOption == TemporalOffset::Use) { + (offsetBehaviour == OffsetBehaviour::Option && + offsetOption == TemporalOffset::Use)) { // Step 6.a. auto epochNanoseconds = GetUTCEpochNanoseconds( dateTime, InstantSpan::fromNanoseconds(offsetNanoseconds)); @@ -151,27 +155,21 @@ bool js::temporal::InterpretISODateTimeOffset( MOZ_ASSERT(offsetOption == TemporalOffset::Prefer || offsetOption == TemporalOffset::Reject); - // FIXME: spec issue - duplicate assertion - // Step 9. - MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( - timeZone, TimeZoneMethod::GetPossibleInstantsFor)); - - // Step 10. Rooted possibleInstants(cx, InstantVector(cx)); if (!GetPossibleInstantsFor(cx, timeZone, temporalDateTime, &possibleInstants)) { return false; } - // Step 11. + // Step 10. if (!possibleInstants.empty()) { - // Step 11.a. + // Step 10.a. Rooted> candidate(cx); for (size_t i = 0; i < possibleInstants.length(); i++) { candidate = possibleInstants[i]; - // Step 11.a.i. + // Step 10.a.i. int64_t candidateNanoseconds; if (!GetOffsetNanosecondsFor(cx, timeZone, candidate, &candidateNanoseconds)) { @@ -180,7 +178,7 @@ bool js::temporal::InterpretISODateTimeOffset( MOZ_ASSERT(std::abs(candidateNanoseconds) < ToNanoseconds(TemporalUnit::Day)); - // Step 11.a.ii. + // Step 10.a.ii. if (candidateNanoseconds == offsetNanoseconds) { auto* unwrapped = candidate.unwrap(cx); if (!unwrapped) { @@ -191,20 +189,20 @@ bool js::temporal::InterpretISODateTimeOffset( return true; } - // Step 11.a.iii. + // Step 10.a.iii. if (matchBehaviour == MatchBehaviour::MatchMinutes) { - // Step 11.a.iii.1. + // Step 10.a.iii.1. int64_t roundedCandidateNanoseconds = RoundNanosecondsToMinutesIncrement(candidateNanoseconds); - // Step 11.a.iii.2. + // Step 10.a.iii.2. if (roundedCandidateNanoseconds == offsetNanoseconds) { auto* unwrapped = candidate.unwrap(cx); if (!unwrapped) { return false; } - // Step 11.a.iii.2.a. + // Step 10.a.iii.2.a. *result = ToInstant(unwrapped); return true; } @@ -212,14 +210,14 @@ bool js::temporal::InterpretISODateTimeOffset( } } - // Step 12. + // Step 11. if (offsetOption == TemporalOffset::Reject) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_ZONED_DATE_TIME_NO_TIME_FOUND); return false; } - // Step 13. + // Step 12. Rooted> instant(cx); if (!DisambiguatePossibleInstants(cx, possibleInstants, timeZone, ToPlainDateTime(temporalDateTime), @@ -232,7 +230,7 @@ bool js::temporal::InterpretISODateTimeOffset( return false; } - // Step 14. + // Step 13. *result = ToInstant(unwrappedInstant); return true; } @@ -430,19 +428,19 @@ static bool ToTemporalZonedDateTime(JSContext* cx, Handle item, bool isUTC; bool hasOffset; int64_t timeZoneOffset; - Rooted timeZoneString(cx); + Rooted timeZoneAnnotation(cx); Rooted calendarString(cx); - if (!ParseTemporalZonedDateTimeString(cx, string, &dateTime, &isUTC, - &hasOffset, &timeZoneOffset, - &timeZoneString, &calendarString)) { + if (!ParseTemporalZonedDateTimeString( + cx, string, &dateTime, &isUTC, &hasOffset, &timeZoneOffset, + &timeZoneAnnotation, &calendarString)) { return false; } // Step 6.d. - MOZ_ASSERT(timeZoneString); + MOZ_ASSERT(timeZoneAnnotation); // Step 6.e. - if (!ToTemporalTimeZone(cx, timeZoneString, &timeZone)) { + if (!ToTemporalTimeZone(cx, timeZoneAnnotation, &timeZone)) { return false; } @@ -624,7 +622,7 @@ struct PlainDateTimeAndInstant { static bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, const PlainDateTime& dateTime, Handle timeZone, - Handle calendar, double days, + Handle calendar, int64_t days, TemporalOverflow overflow, PlainDateTimeAndInstant* result) { // Step 1. (Not applicable in our implementation.) @@ -669,7 +667,7 @@ static bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, bool js::temporal::AddDaysToZonedDateTime( JSContext* cx, const Instant& instant, const PlainDateTime& dateTime, Handle timeZone, Handle calendar, - double days, TemporalOverflow overflow, Instant* result) { + int64_t days, TemporalOverflow overflow, Instant* result) { // Steps 1-7. PlainDateTimeAndInstant dateTimeAndInstant; if (!::AddDaysToZonedDateTime(cx, instant, dateTime, timeZone, calendar, days, @@ -689,7 +687,7 @@ bool js::temporal::AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, const PlainDateTime& dateTime, Handle timeZone, Handle calendar, - double days, Instant* result) { + int64_t days, Instant* result) { // Step 2. auto overflow = TemporalOverflow::Constrain; @@ -700,18 +698,16 @@ bool js::temporal::AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, /** * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, - * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - * [ , precalculatedPlainDateTime [ , options ] ] ) + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) */ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, Handle timeZone, Handle calendar, - const Duration& duration, + const NormalizedDuration& duration, mozilla::Maybe dateTime, Handle maybeOptions, Instant* result) { MOZ_ASSERT(IsValidEpochInstant(epochNanoseconds)); - MOZ_ASSERT(IsValidDuration(duration.date())); - MOZ_ASSERT(IsValidDuration(duration.time())); + MOZ_ASSERT(IsValidDuration(duration)); // Step 1. MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( @@ -725,10 +721,9 @@ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, // Steps 4-5. (Not applicable in our implementation) // Step 6. - if (duration.years == 0 && duration.months == 0 && duration.weeks == 0 && - duration.days == 0) { + if (duration.date == DateDuration{}) { // Step 6.a. - return AddInstant(cx, epochNanoseconds, duration, result); + return AddInstant(cx, epochNanoseconds, duration.time, result); } // Step 7. (Not applicable in our implementation) @@ -748,7 +743,8 @@ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, auto& [date, time] = temporalDateTime; // Step 10. - if (duration.years == 0 && duration.months == 0 && duration.weeks == 0) { + if (duration.date.years == 0 && duration.date.months == 0 && + duration.date.weeks == 0) { // Step 10.a. auto overflow = TemporalOverflow::Constrain; if (maybeOptions) { @@ -760,13 +756,13 @@ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, // Step 10.b. Instant intermediate; if (!AddDaysToZonedDateTime(cx, epochNanoseconds, temporalDateTime, - timeZone, calendar.receiver(), duration.days, - overflow, &intermediate)) { + timeZone, calendar.receiver(), + duration.date.days, overflow, &intermediate)) { return false; } // Step 10.c. - return AddInstant(cx, intermediate, duration.time(), result); + return AddInstant(cx, intermediate, duration.time, result); } // Step 11. @@ -777,13 +773,13 @@ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, const auto& datePart = date; // Step 13. - auto dateDuration = duration.date(); + const auto& dateDuration = duration.date; // Step 14. PlainDate addedDate; if (maybeOptions) { - if (!CalendarDateAdd(cx, calendar, datePart, dateDuration, maybeOptions, - &addedDate)) { + if (!temporal::CalendarDateAdd(cx, calendar, datePart, dateDuration, + maybeOptions, &addedDate)) { return false; } } else { @@ -808,18 +804,17 @@ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, } // Step 17. - return AddInstant(cx, intermediateInstant, duration.time(), result); + return AddInstant(cx, intermediateInstant, duration.time, result); } /** * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, - * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - * [ , precalculatedPlainDateTime [ , options ] ] ) + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) */ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, Handle timeZone, Handle calendar, - const Duration& duration, + const NormalizedDuration& duration, Handle maybeOptions, Instant* result) { return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, duration, mozilla::Nothing(), maybeOptions, result); @@ -827,84 +822,94 @@ static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, /** * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, - * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - * [ , precalculatedPlainDateTime [ , options ] ] ) + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) */ bool js::temporal::AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, Handle timeZone, Handle calendar, - const Duration& duration, Instant* result) { + const NormalizedDuration& duration, + Instant* result) { return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, duration, mozilla::Nothing(), nullptr, result); } /** * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, - * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - * [ , precalculatedPlainDateTime [ , options ] ] ) + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) */ -bool js::temporal::AddZonedDateTime( - JSContext* cx, const Instant& epochNanoseconds, - Handle timeZone, Handle calendar, - const Duration& duration, const PlainDateTime& dateTime, Instant* result) { +bool js::temporal::AddZonedDateTime(JSContext* cx, + const Instant& epochNanoseconds, + Handle timeZone, + Handle calendar, + const NormalizedDuration& duration, + const PlainDateTime& dateTime, + Instant* result) { return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, duration, mozilla::SomeRef(dateTime), nullptr, result); } -double js::temporal::NanosecondsAndDays::daysNumber() const { - if (days) { - return BigInt::numberValue(days); - } - return double(daysInt); +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool js::temporal::AddZonedDateTime(JSContext* cx, + const Instant& epochNanoseconds, + Handle timeZone, + Handle calendar, + const DateDuration& duration, + Instant* result) { + return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, + {duration, {}}, mozilla::Nothing(), nullptr, + result); } -void js::temporal::NanosecondsAndDays::trace(JSTracer* trc) { - if (days) { - TraceRoot(trc, &days, "NanosecondsAndDays::days"); - } +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool js::temporal::AddZonedDateTime(JSContext* cx, + const Instant& epochNanoseconds, + Handle timeZone, + Handle calendar, + const DateDuration& duration, + const PlainDateTime& dateTime, + Instant* result) { + return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, + {duration, {}}, mozilla::SomeRef(dateTime), nullptr, + result); } /** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , * precalculatedPlainDateTime ] ) */ -static bool NanosecondsToDays( - JSContext* cx, const InstantSpan& nanoseconds, +static bool NormalizedTimeDurationToDays( + JSContext* cx, const NormalizedTimeDuration& duration, Handle zonedRelativeTo, Handle timeZone, mozilla::Maybe precalculatedPlainDateTime, - MutableHandle result) { - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + NormalizedTimeAndDays* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); // Step 1. - if (nanoseconds == InstantSpan{}) { - result.set(NanosecondsAndDays::from( - int64_t(0), InstantSpan{}, - InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day)))); - return true; - } + int32_t sign = NormalizedTimeDurationSign(duration); // Step 2. - int32_t sign = nanoseconds < InstantSpan{} ? -1 : 1; + if (sign == 0) { + *result = {int64_t(0), int64_t(0), ToNanoseconds(TemporalUnit::Day)}; + return true; + } // Step 3. - auto startNs = zonedRelativeTo.instant(); - auto calendar = zonedRelativeTo.calendar(); + const auto& startNs = zonedRelativeTo.instant(); // Step 5. - // - // NB: This addition can't overflow, because we've checked that |nanoseconds| - // can be represented as an InstantSpan value. - auto endNs = startNs + nanoseconds; - - // Step 6. - if (!IsValidEpochInstant(endNs)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_INSTANT_INVALID); + Instant endNs; + if (!AddInstant(cx, startNs, duration, &endNs)) { return false; } - // Steps 4 and 8. + // Steps 4 and 7. PlainDateTime startDateTime; if (!precalculatedPlainDateTime) { if (!GetPlainDateTimeFor(cx, timeZone, startNs, &startDateTime)) { @@ -914,145 +919,157 @@ static bool NanosecondsToDays( startDateTime = *precalculatedPlainDateTime; } - // Steps 7 and 9. + // Steps 6 and 8. PlainDateTime endDateTime; if (!GetPlainDateTimeFor(cx, timeZone, endNs, &endDateTime)) { return false; } - // Steps 10-11. (Not applicable in our implementation.) + // Steps 9-10. (Not applicable in our implementation.) - // Step 12. - // - // Overflows in step 21 can be safely ignored, because they take too long to - // happen for int64. - int64_t days = DaysUntil(startDateTime.date, endDateTime.date); + // Step 11. + int32_t days = DaysUntil(startDateTime.date, endDateTime.date); + MOZ_ASSERT(std::abs(days) <= MaxEpochDaysDuration); - // Step 13. + // Step 12. int32_t timeSign = CompareTemporalTime(startDateTime.time, endDateTime.time); - // Steps 14-15. + // Steps 13-14. if (days > 0 && timeSign > 0) { days -= 1; } else if (days < 0 && timeSign < 0) { days += 1; } - // Step 16. + // Step 15. PlainDateTimeAndInstant relativeResult; - if (!::AddDaysToZonedDateTime(cx, startNs, startDateTime, timeZone, calendar, - days, TemporalOverflow::Constrain, - &relativeResult)) { + if (!::AddDaysToZonedDateTime(cx, startNs, startDateTime, timeZone, + zonedRelativeTo.calendar(), days, + TemporalOverflow::Constrain, &relativeResult)) { return false; } MOZ_ASSERT(IsValidISODateTime(relativeResult.dateTime)); MOZ_ASSERT(IsValidEpochInstant(relativeResult.instant)); - // Step 17. - if (sign > 0) { - // Step 17.a. - while (days > 0 && relativeResult.instant > endNs) { - // This loop can iterate indefinitely when given a specially crafted - // time zone object, so we need to check for interrupts. - if (!CheckForInterrupt(cx)) { - return false; - } - - // Step 17.a.i. - days -= 1; + // Step 16. + if (sign > 0 && days > 0 && relativeResult.instant > endNs) { + // Step 16.a. + days -= 1; - // Step 17.a.ii. - if (!::AddDaysToZonedDateTime(cx, startNs, startDateTime, timeZone, - calendar, days, TemporalOverflow::Constrain, - &relativeResult)) { - return false; - } - MOZ_ASSERT(IsValidISODateTime(relativeResult.dateTime)); - MOZ_ASSERT(IsValidEpochInstant(relativeResult.instant)); + // Step 16.b. + if (!::AddDaysToZonedDateTime( + cx, startNs, startDateTime, timeZone, zonedRelativeTo.calendar(), + days, TemporalOverflow::Constrain, &relativeResult)) { + return false; + } + MOZ_ASSERT(IsValidISODateTime(relativeResult.dateTime)); + MOZ_ASSERT(IsValidEpochInstant(relativeResult.instant)); + + // Step 16.c. + if (days > 0 && relativeResult.instant > endNs) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCONSISTENT_INSTANT); + return false; } - MOZ_ASSERT_IF(days > 0, relativeResult.instant <= endNs); } MOZ_ASSERT_IF(days == 0, relativeResult.instant == startNs); - // Step 18. + // Step 17. (Inlined NormalizedTimeDurationFromEpochNanosecondsDifference) auto ns = endNs - relativeResult.instant; MOZ_ASSERT(IsValidInstantSpan(ns)); - // Steps 19-21. - InstantSpan dayLengthNs{}; - while (true) { - // This loop can iterate indefinitely when given a specially crafted time - // zone object, so we need to check for interrupts. - if (!CheckForInterrupt(cx)) { - return false; - } + // Step 18. + PlainDateTimeAndInstant oneDayFarther; + if (!::AddDaysToZonedDateTime(cx, relativeResult.instant, + relativeResult.dateTime, timeZone, + zonedRelativeTo.calendar(), sign, + TemporalOverflow::Constrain, &oneDayFarther)) { + return false; + } + MOZ_ASSERT(IsValidISODateTime(oneDayFarther.dateTime)); + MOZ_ASSERT(IsValidEpochInstant(oneDayFarther.instant)); + + // Step 19. (Inlined NormalizedTimeDurationFromEpochNanosecondsDifference) + auto dayLengthNs = oneDayFarther.instant - relativeResult.instant; + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + + // clang-format off + // + // ns = endNs - relativeResult.instant + // dayLengthNs = oneDayFarther.instant - relativeResult.instant + // oneDayLess = ns - dayLengthNs + // = (endNs - relativeResult.instant) - (oneDayFarther.instant - relativeResult.instant) + // = endNs - relativeResult.instant - oneDayFarther.instant + relativeResult.instant + // = endNs - oneDayFarther.instant + // + // |endNs| and |oneDayFarther.instant| are both valid epoch instant values, + // so the difference |oneDayLess| is a valid epoch instant difference value. + // + // clang-format on + // Step 20. (Inlined SubtractNormalizedTimeDuration) + auto oneDayLess = ns - dayLengthNs; + MOZ_ASSERT(IsValidInstantSpan(oneDayLess)); + MOZ_ASSERT(oneDayLess == (endNs - oneDayFarther.instant)); + + // Step 21. + if (oneDayLess == InstantSpan{} || + ((oneDayLess < InstantSpan{}) == (sign < 0))) { // Step 21.a. + ns = oneDayLess; + + // Step 21.b. + relativeResult = oneDayFarther; + + // Step 21.c. + days += sign; + + // Step 21.d. PlainDateTimeAndInstant oneDayFarther; if (!::AddDaysToZonedDateTime( cx, relativeResult.instant, relativeResult.dateTime, timeZone, - calendar, sign, TemporalOverflow::Constrain, &oneDayFarther)) { + zonedRelativeTo.calendar(), sign, TemporalOverflow::Constrain, + &oneDayFarther)) { return false; } MOZ_ASSERT(IsValidISODateTime(oneDayFarther.dateTime)); MOZ_ASSERT(IsValidEpochInstant(oneDayFarther.instant)); - // Step 21.b. + // Step 21.e. (Inlined NormalizedTimeDurationFromEpochNanosecondsDifference) dayLengthNs = oneDayFarther.instant - relativeResult.instant; MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); // clang-format off // - // First iteration: - // - // ns = endNs - relativeResult.instant - // dayLengthNs = oneDayFarther.instant - relativeResult.instant - // diff = ns - dayLengthNs - // = (endNs - relativeResult.instant) - (oneDayFarther.instant - relativeResult.instant) - // = endNs - relativeResult.instant - oneDayFarther.instant + relativeResult.instant - // = endNs - oneDayFarther.instant - // - // Second iteration: - // - // ns = diff' + // ns = oneDayLess' // = endNs - oneDayFarther.instant' // relativeResult.instant = oneDayFarther.instant' // dayLengthNs = oneDayFarther.instant - relativeResult.instant // = oneDayFarther.instant - oneDayFarther.instant' - // diff = ns - dayLengthNs - // = (endNs - oneDayFarther.instant') - (oneDayFarther.instant - oneDayFarther.instant') - // = endNs - oneDayFarther.instant' - oneDayFarther.instant + oneDayFarther.instant' - // = endNs - oneDayFarther.instant - // - // Where |diff'| and |oneDayFarther.instant'| denote the variables from the - // previous iteration. + // oneDayLess = ns - dayLengthNs + // = (endNs - oneDayFarther.instant') - (oneDayFarther.instant - oneDayFarther.instant') + // = endNs - oneDayFarther.instant' - oneDayFarther.instant + oneDayFarther.instant' + // = endNs - oneDayFarther.instant // - // This repeats for all following iterations. + // Where |oneDayLess'| and |oneDayFarther.instant'| denote the variables + // from before this if-statement block. // // |endNs| and |oneDayFarther.instant| are both valid epoch instant values, - // so the difference is a valid epoch instant difference value, too. + // so the difference |oneDayLess| is a valid epoch instant difference value. // // clang-format on - // Step 21.c. - auto diff = ns - dayLengthNs; - MOZ_ASSERT(IsValidInstantSpan(diff)); - MOZ_ASSERT(diff == (endNs - oneDayFarther.instant)); - - if (diff == InstantSpan{} || ((diff < InstantSpan{}) == (sign < 0))) { - // Step 21.c.i. - ns = diff; - - // Step 21.c.ii. - relativeResult = oneDayFarther; - - // Step 21.c.iii. - days += sign; - } else { - // Step 21.d. - break; + // Step 21.f. + auto oneDayLess = ns - dayLengthNs; + if (oneDayLess == InstantSpan{} || + ((oneDayLess < InstantSpan{}) == (sign < 0))) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCONSISTENT_INSTANT); + return false; } } @@ -1075,15 +1092,6 @@ static bool NanosecondsToDays( MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); MOZ_ASSERT(IsValidInstantSpan(ns)); - // FIXME: spec issue - rewrite steps 24-25 as: - // - // If sign = -1, then - // If nanoseconds > 0, throw a RangeError. - // Else, - // Assert: nanoseconds ≥ 0. - // - // https://github.com/tc39/proposal-temporal/issues/2530 - // Steps 24-25. if (sign < 0) { if (ns > InstantSpan{}) { @@ -1096,39 +1104,57 @@ static bool NanosecondsToDays( MOZ_ASSERT(ns >= InstantSpan{}); } - // Step 26. - MOZ_ASSERT(ns.abs() < dayLengthNs.abs()); + // Steps 26-27. + dayLengthNs = dayLengthNs.abs(); + MOZ_ASSERT(ns.abs() < dayLengthNs); - // Step 27. - result.set(NanosecondsAndDays::from(days, ns, dayLengthNs.abs())); + // Step 28. + constexpr auto maxDayLength = Int128{1} << 53; + auto dayLengthNanos = dayLengthNs.toNanoseconds(); + if (dayLengthNanos >= maxDayLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCORRECT_SIGN, + "days"); + return false; + } + + auto timeNanos = ns.toNanoseconds(); + MOZ_ASSERT(timeNanos == Int128{int64_t(timeNanos)}, + "abs(ns) < dayLengthNs < 2**53 implies that |ns| fits in int64"); + + // Step 29. + static_assert(std::numeric_limits::max() <= + ((int64_t(1) << 53) / (24 * 60 * 60))); + + // Step 30. + *result = {int64_t(days), int64_t(timeNanos), int64_t(dayLengthNanos)}; return true; } /** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , * precalculatedPlainDateTime ] ) */ -bool js::temporal::NanosecondsToDays(JSContext* cx, - const InstantSpan& nanoseconds, - Handle zonedRelativeTo, - Handle timeZone, - MutableHandle result) { - return ::NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, - mozilla::Nothing(), result); +bool js::temporal::NormalizedTimeDurationToDays( + JSContext* cx, const NormalizedTimeDuration& duration, + Handle zonedRelativeTo, Handle timeZone, + NormalizedTimeAndDays* result) { + return ::NormalizedTimeDurationToDays(cx, duration, zonedRelativeTo, timeZone, + mozilla::Nothing(), result); } /** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , * precalculatedPlainDateTime ] ) */ -bool js::temporal::NanosecondsToDays( - JSContext* cx, const InstantSpan& nanoseconds, +bool js::temporal::NormalizedTimeDurationToDays( + JSContext* cx, const NormalizedTimeDuration& duration, Handle zonedRelativeTo, Handle timeZone, const PlainDateTime& precalculatedPlainDateTime, - MutableHandle result) { - return ::NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, - mozilla::SomeRef(precalculatedPlainDateTime), - result); + NormalizedTimeAndDays* result) { + return ::NormalizedTimeDurationToDays( + cx, duration, zonedRelativeTo, timeZone, + mozilla::SomeRef(precalculatedPlainDateTime), result); } /** @@ -1139,27 +1165,22 @@ static bool DifferenceZonedDateTime( JSContext* cx, const Instant& ns1, const Instant& ns2, Handle timeZone, Handle calendar, TemporalUnit largestUnit, Handle maybeOptions, - mozilla::Maybe precalculatedPlainDateTime, - Duration* result) { + const PlainDateTime& precalculatedPlainDateTime, + NormalizedDuration* result) { MOZ_ASSERT(IsValidEpochInstant(ns1)); MOZ_ASSERT(IsValidEpochInstant(ns2)); // Steps 1. if (ns1 == ns2) { - *result = {}; + *result = CreateNormalizedDurationRecord({}, {}); return true; } + // FIXME: spec issue - precalculatedPlainDateTime is never undefined + // https://github.com/tc39/proposal-temporal/issues/2822 + // Steps 2-3. - PlainDateTime startDateTime; - if (!precalculatedPlainDateTime) { - // Steps 2.a-b. - if (!GetPlainDateTimeFor(cx, timeZone, ns1, &startDateTime)) { - return false; - } - } else { - startDateTime = *precalculatedPlainDateTime; - } + const auto& startDateTime = precalculatedPlainDateTime; // Steps 4-5. PlainDateTime endDateTime; @@ -1168,65 +1189,100 @@ static bool DifferenceZonedDateTime( } // Step 6. - Duration dateDifference; - if (maybeOptions) { - if (!DifferenceISODateTime(cx, startDateTime, endDateTime, calendar, - largestUnit, maybeOptions, &dateDifference)) { - return false; - } - } else { - if (!DifferenceISODateTime(cx, startDateTime, endDateTime, calendar, - largestUnit, &dateDifference)) { - return false; - } - } + int32_t sign = (ns2 - ns1 < InstantSpan{}) ? -1 : 1; // Step 7. - Instant intermediateNs; - if (!AddZonedDateTime(cx, ns1, timeZone, calendar, - { - dateDifference.years, - dateDifference.months, - dateDifference.weeks, - }, - startDateTime, &intermediateNs)) { - return false; - } - MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); + int32_t maxDayCorrection = 1 + (sign > 0); // Step 8. - auto timeRemainder = ns2 - intermediateNs; - MOZ_ASSERT(IsValidInstantSpan(timeRemainder)); + int32_t dayCorrection = 0; // Step 9. - Rooted intermediate( - cx, - ZonedDateTime{intermediateNs, timeZone.receiver(), calendar.receiver()}); + auto timeDuration = DifferenceTime(startDateTime.time, endDateTime.time); // Step 10. - Rooted nanosAndDays(cx); - if (!NanosecondsToDays(cx, timeRemainder, intermediate, timeZone, - &nanosAndDays)) { - return false; + if (NormalizedTimeDurationSign(timeDuration) == -sign) { + dayCorrection += 1; } - // Step 11. - TimeDuration timeDifference; - if (!BalanceTimeDuration(cx, nanosAndDays.nanoseconds(), TemporalUnit::Hour, - &timeDifference)) { - return false; + // Steps 11-12. + Rooted intermediateDateTime(cx); + while (dayCorrection <= maxDayCorrection) { + // Step 12.a. + auto intermediateDate = + BalanceISODate(endDateTime.date.year, endDateTime.date.month, + endDateTime.date.day - dayCorrection * sign); + + // FIXME: spec issue - CreateTemporalDateTime is fallible + // https://github.com/tc39/proposal-temporal/issues/2824 + + // Step 12.b. + if (!CreateTemporalDateTime(cx, {intermediateDate, startDateTime.time}, + calendar.receiver(), &intermediateDateTime)) { + return false; + } + + // Steps 12.c-d. + Instant intermediateInstant; + if (!GetInstantFor(cx, timeZone, intermediateDateTime, + TemporalDisambiguation::Compatible, + &intermediateInstant)) { + return false; + } + + // Step 12.e. + auto norm = NormalizedTimeDurationFromEpochNanosecondsDifference( + ns2, intermediateInstant); + + // Step 12.f. + int32_t timeSign = NormalizedTimeDurationSign(norm); + + // Step 12.g. + if (sign != -timeSign) { + // Step 13.a. + const auto& date1 = startDateTime.date; + MOZ_ASSERT(ISODateTimeWithinLimits(date1)); + + // Step 13.b. + const auto& date2 = intermediateDate; + MOZ_ASSERT(ISODateTimeWithinLimits(date2)); + + // Step 13.c. + auto dateLargestUnit = std::min(largestUnit, TemporalUnit::Day); + + // Steps 13.d-e. + // + // The spec performs an unnecessary copy operation. As an optimization, we + // omit this copy. + auto untilOptions = maybeOptions; + + // Step 13.f. + DateDuration dateDifference; + if (untilOptions) { + if (!DifferenceDate(cx, calendar, date1, date2, dateLargestUnit, + untilOptions, &dateDifference)) { + return false; + } + } else { + if (!DifferenceDate(cx, calendar, date1, date2, dateLargestUnit, + &dateDifference)) { + return false; + } + } + + // Step 13.g. + return CreateNormalizedDurationRecord(cx, dateDifference, norm, result); + } + + // Step 12.h. + dayCorrection += 1; } - // Step 12. - *result = { - dateDifference.years, dateDifference.months, - dateDifference.weeks, nanosAndDays.daysNumber(), - timeDifference.hours, timeDifference.minutes, - timeDifference.seconds, timeDifference.milliseconds, - timeDifference.microseconds, timeDifference.nanoseconds, - }; - MOZ_ASSERT(IsValidDuration(*result)); - return true; + // Steps 14-15. + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCONSISTENT_INSTANT); + return false; } /** @@ -1237,10 +1293,10 @@ bool js::temporal::DifferenceZonedDateTime( JSContext* cx, const Instant& ns1, const Instant& ns2, Handle timeZone, Handle calendar, TemporalUnit largestUnit, const PlainDateTime& precalculatedPlainDateTime, - Duration* result) { - return ::DifferenceZonedDateTime( - cx, ns1, ns2, timeZone, calendar, largestUnit, nullptr, - mozilla::SomeRef(precalculatedPlainDateTime), result); + NormalizedDuration* result) { + return ::DifferenceZonedDateTime(cx, ns1, ns2, timeZone, calendar, + largestUnit, nullptr, + precalculatedPlainDateTime, result); } /** @@ -1286,46 +1342,6 @@ static bool TimeZoneEqualsOrThrow(JSContext* cx, Handle one, return false; } -/** - * RoundISODateTime ( year, month, day, hour, minute, second, millisecond, - * microsecond, nanosecond, increment, unit, roundingMode [ , dayLength ] ) - */ -static bool RoundISODateTime(JSContext* cx, const PlainDateTime& dateTime, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, - const InstantSpan& dayLength, - PlainDateTime* result) { - MOZ_ASSERT(IsValidInstantSpan(dayLength)); - MOZ_ASSERT(dayLength > (InstantSpan{})); - - const auto& [date, time] = dateTime; - - // Step 1. - MOZ_ASSERT(IsValidISODateTime(dateTime)); - MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); - - // Step 2. (Not applicable in our implementation.) - - // Step 3. - auto roundedTime = RoundTime(time, increment, unit, roundingMode, dayLength); - - // |dayLength| can be as small as 1, so the number of rounded days can be as - // large as the number of nanoseconds in |time|. - MOZ_ASSERT(0 <= roundedTime.days && - roundedTime.days < ToNanoseconds(TemporalUnit::Day)); - - // Step 4. - PlainDate balanceResult; - if (!BalanceISODate(cx, date.year, date.month, - int64_t(date.day) + roundedTime.days, &balanceResult)) { - return false; - } - - // Step 5. - *result = {balanceResult, roundedTime.time}; - return true; -} - /** * DifferenceTemporalZonedDateTime ( operation, zonedDateTime, other, options ) */ @@ -1385,20 +1401,24 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, MOZ_ASSERT(settings.smallestUnit >= settings.largestUnit); // Step 6.a. - Duration difference; - if (!DifferenceInstant(cx, zonedDateTime.instant(), other.instant(), - settings.roundingIncrement, settings.smallestUnit, - settings.largestUnit, settings.roundingMode, - &difference)) { + auto difference = DifferenceInstant( + zonedDateTime.instant(), other.instant(), settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode); + + // Step 6.b. + TimeDuration balancedTime; + if (!BalanceTimeDuration(cx, difference, settings.largestUnit, + &balancedTime)) { return false; } - // Step 6.b. + // Step 6.c. + auto duration = balancedTime.toDuration(); if (operation == TemporalDifference::Since) { - difference = difference.negate(); + duration = duration.negate(); } - auto* result = CreateTemporalDuration(cx, difference); + auto* result = CreateTemporalDuration(cx, duration); if (!result) { return false; } @@ -1407,15 +1427,12 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, return true; } - // FIXME: spec issue - move this step next to the calendar validation? - // https://github.com/tc39/proposal-temporal/issues/2533 - - // Step 7. + // Steps 7-8. if (!TimeZoneEqualsOrThrow(cx, zonedDateTime.timeZone(), other.timeZone())) { return false; } - // Step 8. + // Step 9. if (zonedDateTime.instant() == other.instant()) { auto* obj = CreateTemporalDuration(cx, {}); if (!obj) { @@ -1426,7 +1443,7 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, return true; } - // Step 9. + // Step 10. Rooted timeZone(cx); if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), { @@ -1437,7 +1454,7 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, return false; } - // Step 10. + // Step 11. Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), { @@ -1448,14 +1465,14 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, return false; } - // Steps 11-12. + // Steps 12-13. PlainDateTime precalculatedPlainDateTime; if (!GetPlainDateTimeFor(cx, timeZone, zonedDateTime.instant(), &precalculatedPlainDateTime)) { return false; } - // Step 13. + // Step 14. Rooted plainRelativeTo( cx, CreateTemporalDate(cx, precalculatedPlainDateTime.date, calendar.receiver())); @@ -1463,23 +1480,12 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, return false; } - // Step 14. - if (resolvedOptions) { - Rooted largestUnitValue( - cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); - if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, - largestUnitValue)) { - return false; - } - } - // Step 15. - Duration difference; - if (!::DifferenceZonedDateTime( - cx, zonedDateTime.instant(), other.instant(), timeZone, calendar, - settings.largestUnit, resolvedOptions, - mozilla::SomeRef(precalculatedPlainDateTime), - &difference)) { + NormalizedDuration difference; + if (!::DifferenceZonedDateTime(cx, zonedDateTime.instant(), other.instant(), + timeZone, calendar, settings.largestUnit, + resolvedOptions, precalculatedPlainDateTime, + &difference)) { return false; } @@ -1489,59 +1495,76 @@ static bool DifferenceTemporalZonedDateTime(JSContext* cx, settings.roundingIncrement == Increment{1}; // Step 17. - if (roundingGranularityIsNoop) { - if (operation == TemporalDifference::Since) { - difference = difference.negate(); + if (!roundingGranularityIsNoop) { + // Steps 17.a-b. + NormalizedDuration roundResult; + if (!RoundDuration(cx, difference, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + plainRelativeTo, calendar, zonedDateTime, timeZone, + precalculatedPlainDateTime, &roundResult)) { + return false; } - auto* obj = CreateTemporalDuration(cx, difference); - if (!obj) { + // Step 17.c. + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, roundResult.time, zonedDateTime, + timeZone, &timeAndDays)) { return false; } - args.rval().setObject(*obj); - return true; - } + // Step 17.d. + int64_t days = roundResult.date.days + timeAndDays.days; + + // Step 17.e. + auto toAdjust = NormalizedDuration{ + { + roundResult.date.years, + roundResult.date.months, + roundResult.date.weeks, + days, + }, + NormalizedTimeDuration::fromNanoseconds(timeAndDays.time), + }; + NormalizedDuration adjustResult; + if (!AdjustRoundedDurationDays(cx, toAdjust, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + zonedDateTime, calendar, timeZone, + precalculatedPlainDateTime, &adjustResult)) { + return false; + } - // Steps 18-19. - Duration roundResult; - if (!RoundDuration(cx, difference, settings.roundingIncrement, - settings.smallestUnit, settings.roundingMode, - plainRelativeTo, calendar, zonedDateTime, timeZone, - precalculatedPlainDateTime, &roundResult)) { - return false; - } + // Step 17.f. + DateDuration balanceResult; + if (!temporal::BalanceDateDurationRelative( + cx, adjustResult.date, settings.largestUnit, settings.smallestUnit, + plainRelativeTo, calendar, &balanceResult)) { + return false; + } - // Step 20. - Duration adjustResult; - if (!AdjustRoundedDurationDays(cx, roundResult, settings.roundingIncrement, - settings.smallestUnit, settings.roundingMode, - zonedDateTime, calendar, timeZone, - precalculatedPlainDateTime, &adjustResult)) { - return false; + // Step 17.g. + if (!CombineDateAndNormalizedTimeDuration(cx, balanceResult, + adjustResult.time, &difference)) { + return false; + } } - // Step 21. - DateDuration balanceResult; - if (!temporal::BalanceDateDurationRelative( - cx, adjustResult.date(), settings.largestUnit, settings.smallestUnit, - plainRelativeTo, calendar, &balanceResult)) { - return false; - } + // Step 18. + auto timeDuration = BalanceTimeDuration(difference.time, TemporalUnit::Hour); - // Step 22. - auto result = Duration{ - balanceResult.years, balanceResult.months, - balanceResult.weeks, balanceResult.days, - adjustResult.hours, adjustResult.minutes, - adjustResult.seconds, adjustResult.milliseconds, - adjustResult.microseconds, adjustResult.nanoseconds, + // Step 19. + auto duration = Duration{ + double(difference.date.years), double(difference.date.months), + double(difference.date.weeks), double(difference.date.days), + double(timeDuration.hours), double(timeDuration.minutes), + double(timeDuration.seconds), double(timeDuration.milliseconds), + timeDuration.microseconds, timeDuration.nanoseconds, }; if (operation == TemporalDifference::Since) { - result = result.negate(); + duration = duration.negate(); } + MOZ_ASSERT(IsValidDuration(duration)); - auto* obj = CreateTemporalDuration(cx, result); + auto* obj = CreateTemporalDuration(cx, duration); if (!obj) { return false; } @@ -1607,15 +1630,17 @@ static bool AddDurationToOrSubtractDurationFromZonedDateTime( if (operation == ZonedDateTimeDuration::Subtract) { duration = duration.negate(); } + auto normalized = CreateNormalizedDurationRecord(duration); + // Step 7. Instant resultInstant; if (!::AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, - duration, options, &resultInstant)) { + normalized, options, &resultInstant)) { return false; } MOZ_ASSERT(IsValidEpochInstant(resultInstant)); - // Step 7. + // Step 8. auto* result = CreateTemporalZonedDateTime( cx, resultInstant, timeZone.receiver(), calendar.receiver()); if (!result) { @@ -1764,8 +1789,8 @@ static bool ZonedDateTime_compare(JSContext* cx, unsigned argc, Value* vp) { } // Step 3. - auto oneNs = one.instant(); - auto twoNs = two.instant(); + const auto& oneNs = one.instant(); + const auto& twoNs = two.instant(); args.rval().setInt32(oneNs > twoNs ? 1 : oneNs < twoNs ? -1 : 0); return true; } @@ -2359,7 +2384,7 @@ static bool ZonedDateTime_hoursInDay(JSContext* cx, const CallArgs& args) { } // Step 4. - auto instant = zonedDateTime.instant(); + const auto& instant = zonedDateTime.instant(); // Step 5. PlainDateTime temporalDateTime; @@ -2402,20 +2427,12 @@ static bool ZonedDateTime_hoursInDay(JSContext* cx, const CallArgs& args) { } // Step 14. - auto diffNs = tomorrowInstant - todayInstant; - MOZ_ASSERT(IsValidInstantSpan(diffNs)); + auto diff = tomorrowInstant - todayInstant; + MOZ_ASSERT(IsValidInstantSpan(diff)); // Step 15. - constexpr int32_t secPerHour = 60 * 60; - constexpr int64_t nsPerSec = ToNanoseconds(TemporalUnit::Second); - constexpr double nsPerHour = ToNanoseconds(TemporalUnit::Hour); - - int64_t hours = diffNs.seconds / secPerHour; - int64_t seconds = diffNs.seconds % secPerHour; - int64_t nanoseconds = seconds * nsPerSec + diffNs.nanoseconds; - - double result = double(hours) + double(nanoseconds) / nsPerHour; - args.rval().setNumber(result); + constexpr auto nsPerHour = Int128{ToNanoseconds(TemporalUnit::Hour)}; + args.rval().setNumber(FractionToDouble(diff.toNanoseconds(), nsPerHour)); return true; } @@ -2587,7 +2604,7 @@ static bool ZonedDateTime_offsetNanoseconds(JSContext* cx, auto timeZone = zonedDateTime.timeZone(); // Step 4. - auto instant = zonedDateTime.instant(); + const auto& instant = zonedDateTime.instant(); // Step 5. int64_t offsetNanoseconds; @@ -2622,7 +2639,7 @@ static bool ZonedDateTime_offset(JSContext* cx, const CallArgs& args) { auto timeZone = zonedDateTime.timeZone(); // Step 4. - auto instant = zonedDateTime.instant(); + const auto& instant = zonedDateTime.instant(); // Step 5. JSString* str = GetOffsetStringFor(cx, timeZone, instant); @@ -2658,13 +2675,11 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { if (!temporalZonedDateTimeLike) { return false; } - - // Step 4. - if (!RejectTemporalLikeObject(cx, temporalZonedDateTimeLike)) { + if (!ThrowIfTemporalLikeObject(cx, temporalZonedDateTimeLike)) { return false; } - // Step 5. + // Step 4. Rooted resolvedOptions(cx); if (args.hasDefined(1)) { Rooted options(cx, @@ -2680,7 +2695,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 6. + // Step 5. Rooted calendar(cx); if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), { @@ -2692,7 +2707,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 7. + // Step 6. Rooted timeZone(cx); if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), { @@ -2703,16 +2718,16 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 8. - auto instant = zonedDateTime.instant(); + // Step 7. + const auto& instant = zonedDateTime.instant(); - // Step 9. + // Step 8. int64_t offsetNanoseconds; if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { return false; } - // Step 10. + // Step 9. Rooted dateTime( cx, GetPlainDateTimeFor(cx, instant, calendar.receiver(), offsetNanoseconds)); @@ -2720,7 +2735,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 11. + // Step 10. JS::RootedVector fieldNames(cx); if (!CalendarFields(cx, calendar, {CalendarField::Day, CalendarField::Month, @@ -2729,14 +2744,14 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 12. + // Step 11. Rooted fields(cx, PrepareTemporalFields(cx, dateTime, fieldNames)); if (!fields) { return false; } - // Steps 13-18. + // Steps 12-17. struct TimeField { using FieldName = ImmutableTenuredPtr JSAtomState::*; @@ -2761,7 +2776,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { } } - // Step 19. + // Step 18. JSString* fieldsOffset = FormatUTCOffsetNanoseconds(cx, offsetNanoseconds); if (!fieldsOffset) { return false; @@ -2772,7 +2787,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 20. + // Step 19. if (!AppendSorted(cx, fieldNames.get(), { TemporalField::Hour, @@ -2786,7 +2801,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 21. + // Step 20. Rooted partialZonedDateTime( cx, PreparePartialTemporalFields(cx, temporalZonedDateTimeLike, fieldNames)); @@ -2794,56 +2809,56 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 22. + // Step 21. Rooted mergedFields( cx, CalendarMergeFields(cx, calendar, fields, partialZonedDateTime)); if (!mergedFields) { return false; } - // Step 23. + // Step 22. fields = PrepareTemporalFields(cx, mergedFields, fieldNames, {TemporalField::Offset}); if (!fields) { return false; } - // Step 24-25. + // Step 23-24. auto disambiguation = TemporalDisambiguation::Compatible; if (!ToTemporalDisambiguation(cx, resolvedOptions, &disambiguation)) { return false; } - // Step 26. + // Step 25. auto offset = TemporalOffset::Prefer; if (!ToTemporalOffset(cx, resolvedOptions, &offset)) { return false; } - // Step 27. + // Step 26. PlainDateTime dateTimeResult; if (!InterpretTemporalDateTimeFields(cx, calendar, fields, resolvedOptions, &dateTimeResult)) { return false; } - // Step 28. + // Step 27. Rooted offsetString(cx); if (!GetProperty(cx, fields, fields, cx->names().offset, &offsetString)) { return false; } - // Step 29. + // Step 28. MOZ_ASSERT(offsetString.isString()); - // Step 30. + // Step 29. Rooted offsetStr(cx, offsetString.toString()); int64_t newOffsetNanoseconds; if (!ParseDateTimeUTCOffset(cx, offsetStr, &newOffsetNanoseconds)) { return false; } - // Step 31. + // Step 30. Instant epochNanoseconds; if (!InterpretISODateTimeOffset( cx, dateTimeResult, OffsetBehaviour::Option, newOffsetNanoseconds, @@ -2852,7 +2867,7 @@ static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { return false; } - // Step 32. + // Step 31. auto* result = CreateTemporalZonedDateTime( cx, epochNanoseconds, timeZone.receiver(), calendar.receiver()); if (!result) { @@ -2880,7 +2895,7 @@ static bool ZonedDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { Rooted zonedDateTime( cx, ZonedDateTime{&args.thisv().toObject().as()}); - // Steps 3-4. + // Step 3. (Inlined ToTemporalTimeOrMidnight) PlainTime time = {}; if (args.hasDefined(0)) { if (!ToTemporalTime(cx, args[0], &time)) { @@ -2888,7 +2903,7 @@ static bool ZonedDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { } } - // Step 5. + // Step 4. Rooted timeZone(cx); if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), { @@ -2899,31 +2914,31 @@ static bool ZonedDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { return false; } - // Steps 6 and 8. + // Steps 5 and 7. PlainDateTime plainDateTime; if (!GetPlainDateTimeFor(cx, timeZone, zonedDateTime.instant(), &plainDateTime)) { return false; } - // Step 7. + // Step 6. auto calendar = zonedDateTime.calendar(); - // Step 9. + // Step 8. Rooted resultPlainDateTime(cx); if (!CreateTemporalDateTime(cx, {plainDateTime.date, time}, calendar, &resultPlainDateTime)) { return false; } - // Step 10. + // Step 9. Instant instant; if (!GetInstantFor(cx, timeZone, resultPlainDateTime, TemporalDisambiguation::Compatible, &instant)) { return false; } - // Step 11. + // Step 10. auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone.receiver(), calendar); if (!result) { @@ -3273,57 +3288,101 @@ static bool ZonedDateTime_round(JSContext* cx, const CallArgs& args) { GetPlainDateTimeFor(zonedDateTime.instant(), offsetNanoseconds); // Step 19. - Rooted isoCalendar(cx, CalendarValue(cx->names().iso8601)); - Rooted dtStart(cx); - if (!CreateTemporalDateTime(cx, {temporalDateTime.date, {}}, isoCalendar, - &dtStart)) { - return false; - } + Instant epochNanoseconds; + if (smallestUnit == TemporalUnit::Day) { + // Step 19.a. + Rooted isoCalendar(cx, CalendarValue(cx->names().iso8601)); + Rooted dtStart(cx); + if (!CreateTemporalDateTime(cx, {temporalDateTime.date, {}}, isoCalendar, + &dtStart)) { + return false; + } - // Steps 20-21. - Instant startNs; - if (!GetInstantFor(cx, timeZone, dtStart, TemporalDisambiguation::Compatible, - &startNs)) { - return false; - } + // Step 19.b. + auto dateEnd = + BalanceISODate(temporalDateTime.date.year, temporalDateTime.date.month, + temporalDateTime.date.day + 1); - // Step 22. - Instant endNs; - if (!AddDaysToZonedDateTime(cx, startNs, ToPlainDateTime(dtStart), timeZone, - calendar, 1, &endNs)) { - return false; - } - MOZ_ASSERT(IsValidEpochInstant(endNs)); + // Step 19.c. + Rooted dtEnd(cx); + if (!CreateTemporalDateTime(cx, {dateEnd, {}}, isoCalendar, &dtEnd)) { + return false; + } - // Step 23. - auto dayLengthNs = endNs - startNs; - MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + // Step 19.d. + const auto& thisNs = zonedDateTime.instant(); - // Step 24. - if (dayLengthNs <= InstantSpan{}) { - JS_ReportErrorNumberASCII( - cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_ZONED_DATE_TIME_NON_POSITIVE_DAY_LENGTH); - return false; - } + // Steps 19.e-f. + Instant startNs; + if (!GetInstantFor(cx, timeZone, dtStart, + TemporalDisambiguation::Compatible, &startNs)) { + return false; + } - // Step 25. - PlainDateTime roundResult; - if (!RoundISODateTime(cx, temporalDateTime, roundingIncrement, smallestUnit, - roundingMode, dayLengthNs, &roundResult)) { - return false; - } + // Step 19.g. + if (thisNs < startNs) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCONSISTENT_INSTANT); + return false; + } - // Step 26. - Instant epochNanoseconds; - if (!InterpretISODateTimeOffset( - cx, roundResult, OffsetBehaviour::Option, offsetNanoseconds, timeZone, - TemporalDisambiguation::Compatible, TemporalOffset::Prefer, - MatchBehaviour::MatchExactly, &epochNanoseconds)) { - return false; + // Steps 19.h-i. + Instant endNs; + if (!GetInstantFor(cx, timeZone, dtEnd, TemporalDisambiguation::Compatible, + &endNs)) { + return false; + } + + // Step 19.j. + if (thisNs >= endNs) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCONSISTENT_INSTANT); + return false; + } + + // Step 19.k. + auto dayLengthNs = endNs - startNs; + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + MOZ_ASSERT(dayLengthNs > InstantSpan{}, "dayLengthNs is positive"); + + // Step 19.l. (Inlined NormalizedTimeDurationFromEpochNanosecondsDifference) + auto dayProgressNs = thisNs - startNs; + MOZ_ASSERT(IsValidInstantSpan(dayProgressNs)); + MOZ_ASSERT(dayProgressNs >= InstantSpan{}, "dayProgressNs is non-negative"); + + MOZ_ASSERT(startNs <= thisNs && thisNs < endNs); + MOZ_ASSERT(dayProgressNs < dayLengthNs); + + // Step 19.m. (Inlined RoundNormalizedTimeDurationToIncrement) + auto rounded = + RoundNumberToIncrement(dayProgressNs.toNanoseconds(), + dayLengthNs.toNanoseconds(), roundingMode); + auto roundedDaysNs = InstantSpan::fromNanoseconds(rounded); + MOZ_ASSERT(roundedDaysNs == InstantSpan{} || roundedDaysNs == dayLengthNs); + MOZ_ASSERT(IsValidInstantSpan(roundedDaysNs)); + + // Step 19.n. + epochNanoseconds = startNs + roundedDaysNs; + MOZ_ASSERT(epochNanoseconds == startNs || epochNanoseconds == endNs); + } else { + // Step 20.a. + auto roundResult = RoundISODateTime(temporalDateTime, roundingIncrement, + smallestUnit, roundingMode); + + // Step 20.b. + if (!InterpretISODateTimeOffset( + cx, roundResult, OffsetBehaviour::Option, offsetNanoseconds, + timeZone, TemporalDisambiguation::Compatible, + TemporalOffset::Prefer, MatchBehaviour::MatchExactly, + &epochNanoseconds)) { + return false; + } } + MOZ_ASSERT(IsValidEpochInstant(epochNanoseconds)); - // Step 27. + // Step 22. auto* result = CreateTemporalZonedDateTime(cx, epochNanoseconds, timeZone.receiver(), calendar); if (!result) { @@ -3557,7 +3616,7 @@ static bool ZonedDateTime_startOfDay(JSContext* cx, const CallArgs& args) { auto calendar = zonedDateTime.calendar(); // Step 5. - auto instant = zonedDateTime.instant(); + const auto& instant = zonedDateTime.instant(); // Steps 5-6. PlainDateTime temporalDateTime; @@ -3864,7 +3923,7 @@ static bool ZonedDateTime_getISOFields(JSContext* cx, const CallArgs& args) { Rooted fields(cx, IdValueVector(cx)); // Step 4. - auto instant = zonedDateTime.instant(); + const auto& instant = zonedDateTime.instant(); // Step 5. auto calendar = zonedDateTime.calendar(); diff --git a/js/src/builtin/temporal/ZonedDateTime.h b/js/src/builtin/temporal/ZonedDateTime.h index 73e3a3384f..7d75a33201 100644 --- a/js/src/builtin/temporal/ZonedDateTime.h +++ b/js/src/builtin/temporal/ZonedDateTime.h @@ -8,6 +8,7 @@ #define builtin_temporal_ZonedDateTime_h #include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" #include @@ -69,7 +70,7 @@ inline Instant ToInstant(const ZonedDateTimeObject* zonedDateTime) { return {zonedDateTime->seconds(), zonedDateTime->nanoseconds()}; } -class ZonedDateTime { +class MOZ_STACK_CLASS ZonedDateTime final { Instant instant_; TimeZoneValue timeZone_; CalendarValue calendar_; @@ -125,7 +126,7 @@ ZonedDateTimeObject* CreateTemporalZonedDateTime( bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, const PlainDateTime& dateTime, JS::Handle timeZone, - JS::Handle calendar, double days, + JS::Handle calendar, int64_t days, TemporalOverflow overflow, Instant* result); /** @@ -135,29 +136,46 @@ bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, const PlainDateTime& dateTime, JS::Handle timeZone, - JS::Handle calendar, double days, + JS::Handle calendar, int64_t days, Instant* result); /** * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, - * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - * [ , precalculatedPlainDateTime [ , options ] ] ) + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) */ bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, JS::Handle timeZone, JS::Handle calendar, - const Duration& duration, Instant* result); + const NormalizedDuration& duration, Instant* result); /** * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, - * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - * [ , precalculatedPlainDateTime [ , options ] ] ) + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) */ bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, JS::Handle timeZone, JS::Handle calendar, - const Duration& duration, const PlainDateTime& dateTime, - Instant* result); + const NormalizedDuration& duration, + const PlainDateTime& dateTime, Instant* result); + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, + JS::Handle timeZone, + JS::Handle calendar, + const DateDuration& duration, Instant* result); + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, norm [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, + JS::Handle timeZone, + JS::Handle calendar, + const DateDuration& duration, + const PlainDateTime& dateTime, Instant* result); /** * DifferenceZonedDateTime ( ns1, ns2, timeZoneRec, calendarRec, largestUnit, @@ -169,48 +187,34 @@ bool DifferenceZonedDateTime(JSContext* cx, const Instant& ns1, JS::Handle calendar, TemporalUnit largestUnit, const PlainDateTime& precalculatedPlainDateTime, - Duration* result); - -struct NanosecondsAndDays final { - JS::BigInt* days = nullptr; - int64_t daysInt = 0; - InstantSpan nanoseconds; - InstantSpan dayLength; - - double daysNumber() const; + NormalizedDuration* result); - void trace(JSTracer* trc); - - static NanosecondsAndDays from(int64_t days, const InstantSpan& nanoseconds, - const InstantSpan& dayLength) { - return {nullptr, days, nanoseconds, dayLength}; - } - - static NanosecondsAndDays from(JS::BigInt* days, - const InstantSpan& nanoseconds, - const InstantSpan& dayLength) { - return {days, 0, nanoseconds, dayLength}; - } +struct NormalizedTimeAndDays final { + int64_t days = 0; + int64_t time = 0; + int64_t dayLength = 0; }; /** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , * precalculatedPlainDateTime ] ) */ -bool NanosecondsToDays(JSContext* cx, const InstantSpan& nanoseconds, - JS::Handle zonedRelativeTo, - JS::Handle timeZone, - JS::MutableHandle result); +bool NormalizedTimeDurationToDays(JSContext* cx, + const NormalizedTimeDuration& duration, + JS::Handle zonedRelativeTo, + JS::Handle timeZone, + NormalizedTimeAndDays* result); /** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , * precalculatedPlainDateTime ] ) */ -bool NanosecondsToDays(JSContext* cx, const InstantSpan& nanoseconds, - JS::Handle zonedRelativeTo, - JS::Handle timeZone, - const PlainDateTime& precalculatedPlainDateTime, - JS::MutableHandle result); +bool NormalizedTimeDurationToDays( + JSContext* cx, const NormalizedTimeDuration& duration, + JS::Handle zonedRelativeTo, + JS::Handle timeZone, + const PlainDateTime& precalculatedPlainDateTime, + NormalizedTimeAndDays* result); enum class OffsetBehaviour { Option, Exact, Wall }; @@ -255,26 +259,6 @@ class WrappedPtrOperations { } }; -template -class WrappedPtrOperations { - const auto& object() const { - return static_cast(this)->get(); - } - - public: - double daysNumber() const { return object().daysNumber(); } - - JS::Handle days() const { - return JS::Handle::fromMarkedLocation(&object().days); - } - - int64_t daysInt() const { return object().daysInt; } - - temporal::InstantSpan nanoseconds() const { return object().nanoseconds; } - - temporal::InstantSpan dayLength() const { return object().dayLength; } -}; - } /* namespace js */ #endif /* builtin_temporal_ZonedDateTime_h */ diff --git a/js/src/builtin/temporal/moz.build b/js/src/builtin/temporal/moz.build index ae3bb618ad..3c09960783 100644 --- a/js/src/builtin/temporal/moz.build +++ b/js/src/builtin/temporal/moz.build @@ -17,6 +17,7 @@ if CONFIG["JS_HAS_TEMPORAL_API"]: "Calendar.cpp", "Duration.cpp", "Instant.cpp", + "Int128.cpp", "Int96.cpp", "PlainDate.cpp", "PlainDateTime.cpp", -- cgit v1.2.3