summaryrefslogtreecommitdiffstats
path: root/js/src/builtin/Array.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /js/src/builtin/Array.cpp
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/builtin/Array.cpp')
-rw-r--r--js/src/builtin/Array.cpp572
1 files changed, 520 insertions, 52 deletions
diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp
index 76493eee7f..2ced07b6b9 100644
--- a/js/src/builtin/Array.cpp
+++ b/js/src/builtin/Array.cpp
@@ -10,6 +10,7 @@
#include "mozilla/DebugOnly.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Maybe.h"
+#include "mozilla/ScopeExit.h"
#include "mozilla/SIMD.h"
#include "mozilla/TextUtils.h"
@@ -23,6 +24,7 @@
#include "ds/Sort.h"
#include "jit/InlinableNatives.h"
+#include "jit/TrampolineNatives.h"
#include "js/Class.h"
#include "js/Conversions.h"
#include "js/experimental/JitInfo.h" // JSJitGetterOp, JSJitInfo
@@ -2026,57 +2028,10 @@ static bool FillWithUndefined(JSContext* cx, HandleObject obj, uint32_t start,
return true;
}
-static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj,
- Handle<Value> fval, ComparatorMatchResult comp);
-
-bool js::intrinsic_ArrayNativeSort(JSContext* cx, unsigned argc, Value* vp) {
- // This function is called from the self-hosted Array.prototype.sort
- // implementation. It returns |true| if the array was sorted, otherwise it
- // returns |false| to notify the self-hosted code to perform the sorting.
- CallArgs args = CallArgsFromVp(argc, vp);
- MOZ_ASSERT(args.length() == 1);
-
- HandleValue fval = args[0];
- MOZ_ASSERT(fval.isUndefined() || IsCallable(fval));
-
- ComparatorMatchResult comp;
- if (fval.isObject()) {
- comp = MatchNumericComparator(cx, &fval.toObject());
- if (comp == Match_Failure) {
- return false;
- }
-
- if (comp == Match_None) {
- // Non-optimized user supplied comparators perform much better when
- // called from within a self-hosted sorting function.
- args.rval().setBoolean(false);
- return true;
- }
- } else {
- comp = Match_None;
- }
-
- Rooted<JSObject*> obj(cx, &args.thisv().toObject());
-
- if (!ArrayNativeSortImpl(cx, obj, fval, comp)) {
- return false;
- }
-
- args.rval().setBoolean(true);
- return true;
-}
-
-static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj,
- Handle<Value> fval,
- ComparatorMatchResult comp) {
- uint64_t length;
- if (!GetLengthPropertyInlined(cx, obj, &length)) {
- return false;
- }
- if (length < 2) {
- /* [] and [a] remain unchanged when sorted. */
- return true;
- }
+static bool ArraySortWithoutComparator(JSContext* cx, Handle<JSObject*> obj,
+ uint64_t length,
+ ComparatorMatchResult comp) {
+ MOZ_ASSERT(length > 1);
if (length > UINT32_MAX) {
ReportAllocationOverflow(cx);
@@ -2225,6 +2180,519 @@ static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> 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<JSFunction>()) {
+ return ComparatorKind::Unoptimized;
+ }
+ JSFunction* fun = &comparator->as<JSFunction>();
+ 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.
+//
+// This is separate from ArraySortData::sortWithComparator because it lets the
+// compiler generate better code for ArraySortData::sortWithComparator.
+//
+// https://tc39.es/ecma262/#sec-array.prototype.sort
+// 23.1.3.30 Array.prototype.sort ( comparefn )
+static MOZ_ALWAYS_INLINE bool ArraySortPrologue(JSContext* cx,
+ Handle<Value> thisv,
+ Handle<Value> comparefn,
+ ArraySortData* d, bool* done) {
+ // Step 1.
+ if (MOZ_UNLIKELY(!comparefn.isUndefined() && !IsCallable(comparefn))) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_SORT_ARG);
+ return false;
+ }
+
+ // Step 2.
+ Rooted<JSObject*> obj(cx, ToObject(cx, thisv));
+ if (!obj) {
+ return false;
+ }
+
+ // Step 3.
+ uint64_t length;
+ if (MOZ_UNLIKELY(!GetLengthPropertyInlined(cx, obj, &length))) {
+ return false;
+ }
+
+ // Arrays with less than two elements remain unchanged when sorted.
+ if (length <= 1) {
+ d->setReturnValue(obj);
+ *done = true;
+ return true;
+ }
+
+ // Use a fast path if there's no comparator or if the comparator is a function
+ // that we can pattern match.
+ do {
+ ComparatorMatchResult comp = Match_None;
+ if (comparefn.isObject()) {
+ comp = MatchNumericComparator(cx, &comparefn.toObject());
+ if (comp == Match_Failure) {
+ return false;
+ }
+ if (comp == Match_None) {
+ // Pattern matching failed.
+ break;
+ }
+ }
+ if (!ArraySortWithoutComparator(cx, obj, length, comp)) {
+ return false;
+ }
+ d->setReturnValue(obj);
+ *done = true;
+ return true;
+ } while (false);
+
+ // Ensure length * 2 (used below) doesn't overflow UINT32_MAX.
+ if (MOZ_UNLIKELY(length > UINT32_MAX / 2)) {
+ ReportAllocationOverflow(cx);
+ return false;
+ }
+ uint32_t len = uint32_t(length);
+
+ // Merge sort requires extra scratch space.
+ bool needsScratchSpace = len >= ArraySortData::InsertionSortLimit;
+
+ Rooted<ArraySortData::ValueVector> vec(cx);
+ if (MOZ_UNLIKELY(!vec.reserve(needsScratchSpace ? (2 * len) : len))) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ // Append elements to the vector. Skip holes.
+ if (IsPackedArray(obj)) {
+ Handle<ArrayObject*> array = obj.as<ArrayObject>();
+ const Value* elements = array->getDenseElements();
+ vec.infallibleAppend(elements, len);
+ } else {
+ RootedValue v(cx);
+ for (uint32_t i = 0; i < len; i++) {
+ if (!CheckForInterrupt(cx)) {
+ return false;
+ }
+
+ bool hole;
+ if (!HasAndGetElement(cx, obj, i, &hole, &v)) {
+ return false;
+ }
+ if (hole) {
+ continue;
+ }
+ vec.infallibleAppend(v);
+ }
+ // If there are only holes, the object is already sorted.
+ if (vec.empty()) {
+ d->setReturnValue(obj);
+ *done = true;
+ return true;
+ }
+ }
+
+ uint32_t denseLen = vec.length();
+ if (needsScratchSpace) {
+ MOZ_ALWAYS_TRUE(vec.resize(denseLen * 2));
+ }
+ d->init(obj, &comparefn.toObject(), std::move(vec.get()), len, denseLen);
+
+ // Continue in ArraySortData::sortWithComparator.
+ MOZ_ASSERT(!*done);
+ return true;
+}
+
+static ArraySortResult CallComparatorSlow(ArraySortData* d, const Value& x,
+ const Value& y) {
+ JSContext* cx = d->cx();
+ FixedInvokeArgs<2> callArgs(cx);
+ callArgs[0].set(x);
+ callArgs[1].set(y);
+ Rooted<Value> comparefn(cx, ObjectValue(*d->comparator()));
+ Rooted<Value> rval(cx);
+ if (!js::Call(cx, comparefn, UndefinedHandleValue, callArgs, &rval)) {
+ return ArraySortResult::Failure;
+ }
+ d->setComparatorReturnValue(rval);
+ 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<Value> 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<uint32_t>(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<uint32_t>(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);
+ }
+ }
+
+ // Copy elements to the array.
+ Rooted<JSObject*> obj(cx, d->obj_);
+ if (!SetArrayElements(cx, obj, 0, d->denseLen, d->list)) {
+ return ArraySortResult::Failure;
+ }
+
+ // Re-create any holes that sorted to the end of the array.
+ for (uint32_t i = d->denseLen; i < d->length; i++) {
+ if (!CheckForInterrupt(cx) || !DeletePropertyOrThrow(cx, obj, i)) {
+ return ArraySortResult::Failure;
+ }
+ }
+
+ d->freeMallocData();
+ d->setReturnValue(obj);
+ return ArraySortResult::Done;
+}
+
+// https://tc39.es/ecma262/#sec-array.prototype.sort
+// 23.1.3.30 Array.prototype.sort ( comparefn )
+bool js::array_sort(JSContext* cx, unsigned argc, Value* vp) {
+ AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "sort");
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // If we have a comparator argument, use the JIT trampoline implementation
+ // instead. This avoids a performance cliff (especially with large arrays)
+ // because C++ => JIT calls are much slower than Trampoline => JIT calls.
+ if (args.hasDefined(0) && jit::IsBaselineInterpreterEnabled()) {
+ return CallTrampolineNativeJitCode(cx, jit::TrampolineNative::ArraySort,
+ args);
+ }
+
+ Rooted<ArraySortData> 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.
+ auto freeData =
+ mozilla::MakeScopeExit([&]() { data.get().freeMallocData(); });
+
+ bool done = false;
+ if (!ArraySortPrologue(cx, args.thisv(), args.get(0), data.address(),
+ &done)) {
+ return false;
+ }
+ if (done) {
+ args.rval().set(data.get().returnValue());
+ return true;
+ }
+
+ FixedInvokeArgs<2> callArgs(cx);
+ Rooted<Value> rval(cx);
+
+ while (true) {
+ ArraySortResult res = ArraySortData::sortWithComparator(data.address());
+ switch (res) {
+ case ArraySortResult::Failure:
+ return false;
+
+ case ArraySortResult::Done:
+ freeData.release();
+ args.rval().set(data.get().returnValue());
+ return true;
+
+ case ArraySortResult::CallJS:
+ case ArraySortResult::CallJSSameRealmNoRectifier:
+ MOZ_ASSERT(data.get().comparatorThisValue().isUndefined());
+ MOZ_ASSERT(&args[0].toObject() == data.get().comparator());
+ callArgs[0].set(data.get().comparatorArg(0));
+ callArgs[1].set(data.get().comparatorArg(1));
+ if (!js::Call(cx, args[0], UndefinedHandleValue, callArgs, &rval)) {
+ return false;
+ }
+ data.get().setComparatorReturnValue(rval);
+ break;
+ }
+ }
+}
+
+ArraySortResult js::ArraySortFromJit(JSContext* cx,
+ jit::TrampolineNativeFrameLayout* frame) {
+ // Initialize the ArraySortData class stored in the trampoline frame.
+ void* dataUninit = frame->getFrameData<ArraySortData>();
+ auto* data = new (dataUninit) ArraySortData(cx);
+
+ Rooted<Value> thisv(cx, frame->thisv());
+ Rooted<Value> comparefn(cx);
+ if (frame->numActualArgs() > 0) {
+ comparefn = frame->actualArgs()[0];
+ }
+
+ bool done = false;
+ if (!ArraySortPrologue(cx, thisv, comparefn, data, &done)) {
+ return ArraySortResult::Failure;
+ }
+ if (done) {
+ data->freeMallocData();
+ 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
+}
+
+void ArraySortData::trace(JSTracer* trc) {
+ TraceNullableRoot(trc, &comparator_, "comparator_");
+ TraceRoot(trc, &thisv, "thisv");
+ TraceRoot(trc, &callArgs[0], "callArgs0");
+ TraceRoot(trc, &callArgs[1], "callArgs1");
+ vec.trace(trc);
+ TraceRoot(trc, &item, "item");
+ TraceNullableRoot(trc, &obj_, "obj");
+}
+
bool js::NewbornArrayPush(JSContext* cx, HandleObject obj, const Value& v) {
Handle<ArrayObject*> arr = obj.as<ArrayObject>();
@@ -4846,7 +5314,7 @@ static const JSFunctionSpec array_methods[] = {
/* Perl-ish methods. */
JS_INLINABLE_FN("join", array_join, 1, 0, ArrayJoin),
JS_FN("reverse", array_reverse, 0, 0),
- JS_SELF_HOSTED_FN("sort", "ArraySort", 1, 0),
+ JS_TRAMPOLINE_FN("sort", array_sort, 1, 0, ArraySort),
JS_INLINABLE_FN("push", array_push, 1, 0, ArrayPush),
JS_INLINABLE_FN("pop", array_pop, 0, 0, ArrayPop),
JS_INLINABLE_FN("shift", array_shift, 0, 0, ArrayShift),