Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ES2023 Array.prototype.toSorted #1298

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/hermes/VM/NativeFunctions.def
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ NATIVE_FUNCTION(arrayPrototypeSplice)
NATIVE_FUNCTION(arrayPrototypeToReversed)
NATIVE_FUNCTION(arrayPrototypeToSpliced)
NATIVE_FUNCTION(arrayPrototypeWith)
NATIVE_FUNCTION(arrayPrototypeToSorted)
NATIVE_FUNCTION(asyncFunctionConstructor)
NATIVE_FUNCTION(atob)

Expand Down
1 change: 1 addition & 0 deletions include/hermes/VM/PredefinedStrings.def
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ STR(flatMap, "flatMap")
STR(toReversed, "toReversed")
STR(toSpliced, "toSpliced")
STR(with, "with")
STR(toSorted, "toSorted")

STR(ArrayBuffer, "ArrayBuffer")
STR(byteLength, "byteLength")
Expand Down
318 changes: 220 additions & 98 deletions lib/VM/JSLib/Array.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ Handle<JSObject> createArrayConstructor(Runtime &runtime) {
nullptr,
arrayPrototypeWith,
2);
defineMethod(
runtime,
arrayPrototype,
Predefined::getSymbolID(Predefined::toSorted),
nullptr,
arrayPrototypeToSorted,
1);

auto propValue = runtime.ignoreAllocationFailure(JSObject::getNamed_RJS(
arrayPrototype, runtime, Predefined::getSymbolID(Predefined::values)));
Expand Down Expand Up @@ -1341,114 +1348,112 @@ class StandardSortModel : public SortModel {
/// Perform a sort of a sparse object by querying its properties first.
/// It cannot be a proxy or a host object because they are not guaranteed to
/// be able to list their properties.
CallResult<HermesValue> sortSparse(
CallResult<HermesValue> sortIndexedProperties(
Runtime &runtime,
Handle<JSObject> O,
uint64_t len,
Handle<Callable> compareFn,
uint64_t len) {
bool skipHoles) {
GCScope gcScope{runtime};

assert(
!O->isHostObject() && !O->isProxyObject() &&
"only non-exotic objects can be sparsely sorted");

// This is a "non-fast" object, meaning we need to create a symbol for every
// property name. On the assumption that it is sparse, get all properties
// first, so that we only have to read the existing properties.

auto crNames = JSObject::getOwnPropertyNames(O, runtime, false);
if (crNames == ExecutionStatus::EXCEPTION)
return ExecutionStatus::EXCEPTION;
// Get the underlying storage containing the names.
auto names = runtime.makeHandle((*crNames)->getIndexedStorage(runtime));
if (!names) {
// Indexed storage can be null if there's nothing to store.
return O.getHermesValue();
}

// Find out how many sortable numeric properties we have.
JSArray::StorageType::size_type numProps = 0;
for (JSArray::StorageType::size_type e = names->size(runtime); numProps != e;
++numProps) {
SmallHermesValue hv = names->at(runtime, numProps);
// Stop at the first non-number.
if (!hv.isNumber())
break;
// Stop if the property name is beyond "len".
if (hv.getNumber(runtime) >= len)
break;
}

// If we didn't find any numeric properties, there is nothing to do.
if (numProps == 0)
return O.getHermesValue();

// Create a new array which we will actually sort.
auto crArray = JSArray::create(runtime, numProps, numProps);
if (crArray == ExecutionStatus::EXCEPTION)
return ExecutionStatus::EXCEPTION;
auto array = *crArray;
if (JSArray::setStorageEndIndex(array, runtime, numProps) ==
ExecutionStatus::EXCEPTION) {
// 1. Let items be a new empty List.
auto itemsArrayRes = JSArray::create(runtime, len, len);
if (itemsArrayRes == ExecutionStatus::EXCEPTION)
return ExecutionStatus::EXCEPTION;
}

MutableHandle<> propName{runtime};
MutableHandle<> propVal{runtime};
auto items = *itemsArrayRes;

MutableHandle<> kHandle{runtime};
MutableHandle<> kValueHandle{runtime};
MutableHandle<SymbolID> fromNameTmpStorage{runtime};
MutableHandle<JSObject> fromObj{runtime};
GCScopeMarkerRAII gcMarker{gcScope};

// Copy all sortable properties into the array and delete them from the
// source. Deleting all sortable properties makes it easy to just copy the
// sorted result back in the end.
for (decltype(numProps) i = 0; i != numProps; ++i) {
gcMarker.flush();
// 2. Let k be 0.
uint64_t k = 0;

propName = names->at(runtime, i).unboxToHV(runtime);
auto res = JSObject::getComputed_RJS(O, runtime, propName);
if (res == ExecutionStatus::EXCEPTION)
return ExecutionStatus::EXCEPTION;
// Skip empty values.
if (res->getHermesValue().isEmpty())
continue;
// Here, we diverge a bit from original algorithm by moving checking of
// `skipHoles` outside the loop, so that we have one branch instruction rather
// than two.
if (skipHoles) {
// 3. Repeat, while k < len,
while (k < len) {
gcMarker.flush();

const auto shv = SmallHermesValue::encodeHermesValue(res->get(), runtime);
JSArray::unsafeSetExistingElementAt(*array, runtime, i, shv);
// 3a. Let Pk be ! ToString(𝔽(k)).
kHandle = HermesValue::encodeTrustedNumberValue(k);
ComputedPropertyDescriptor desc;

if (JSObject::deleteComputed(
O, runtime, propName, PropOpFlags().plusThrowOnError()) ==
ExecutionStatus::EXCEPTION) {
return ExecutionStatus::EXCEPTION;
// 3b.i. Let kRead be ? HasProperty(obj, Pk).
if (LLVM_UNLIKELY(
JSObject::getComputedDescriptor(
O, runtime, kHandle, fromObj, fromNameTmpStorage, desc) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

CallResult<PseudoHandle<>> kValueRes =
JSObject::getComputedPropertyValue_RJS(
O, runtime, fromObj, fromNameTmpStorage, desc, kHandle);

if (LLVM_UNLIKELY(kValueRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

// 3d. If kRead is true, then
if (LLVM_LIKELY(!(*kValueRes)->isEmpty())) {
// 3d.ii Append kValue to items
kValueHandle = std::move(*kValueRes);
JSArray::setElementAt(items, runtime, k, kValueHandle);
}

k += 1;
}
}
gcMarker.flush();
// if read-through-holes
else {
// 3. Repeat, while k < len,
while (k < len) {
gcMarker.flush();

{
StandardSortModel sm(runtime, array, compareFn);
if (LLVM_UNLIKELY(
quickSort(&sm, 0u, numProps) == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
}
// 3a. Let Pk be ! ToString(𝔽(k)).
kHandle = HermesValue::encodeTrustedNumberValue(k);
ComputedPropertyDescriptor desc;

// Time to copy back the values.
for (decltype(numProps) i = 0; i != numProps; ++i) {
gcMarker.flush();
// 3b.i. Let kRead be ? HasProperty(obj, Pk).
if (LLVM_UNLIKELY(
JSObject::getComputedDescriptor(
O, runtime, kHandle, fromObj, fromNameTmpStorage, desc) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

auto hv = array->at(runtime, i).unboxToHV(runtime);
assert(
!hv.isEmpty() &&
"empty values cannot appear in the array out of nowhere");
propVal = hv;
CallResult<PseudoHandle<>> kValueRes =
JSObject::getComputedPropertyValue_RJS(
O, runtime, fromObj, fromNameTmpStorage, desc, kHandle);

propName = HermesValue::encodeTrustedNumberValue(i);
if (LLVM_UNLIKELY(kValueRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

if (JSObject::putComputed_RJS(
O, runtime, propName, propVal, PropOpFlags().plusThrowOnError()) ==
ExecutionStatus::EXCEPTION) {
return ExecutionStatus::EXCEPTION;
// 3d. If kRead is true, then
// 3d.i Let kValue be ? Get(obj, Pk).
kValueHandle = std::move(*kValueRes);
// 3d.ii Append kValue to items
JSArray::setElementAt(items, runtime, k, kValueHandle);

// 3.e Set k to k + 1
k += 1;
}
}

return O.getHermesValue();
{
StandardSortModel sm(runtime, items, compareFn);
if (LLVM_UNLIKELY(quickSort(&sm, 0u, len) == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
}

return items.getHermesValue();
}
} // anonymous namespace

Expand Down Expand Up @@ -1478,20 +1483,60 @@ arrayPrototypeSort(void *, Runtime &runtime, NativeArgs args) {
}
uint64_t len = *intRes;

// If we are not sorting a regular dense array, use a special routine which
// first copies all properties into an array.
// Proxies and host objects however are excluded because they are weird.
if (!O->isProxyObject() && !O->isHostObject() && !O->hasFastIndexProperties())
return sortSparse(runtime, O, compareFn, len);
// 5. Let sortedList be ? SortIndexedProperties(obj, len, SortCompare,
// skip-holes).
auto sortedListRes = sortIndexedProperties(runtime, O, len, compareFn, true);
if (LLVM_UNLIKELY(sortedListRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

// This is the "fast" path. We are sorting an array with indexed storage.
StandardSortModel sm(runtime, O, compareFn);
Handle<> sortedList = runtime.makeHandle(*sortedListRes);
auto sortedListArr = Handle<JSArray>::dyn_vmcast(sortedList);

// Use our custom sort routine. We can't use std::sort because it performs
// optimizations that allow it to bypass calls to std::swap, but our swap
// function is special, since it needs to use the internal Object functions.
if (LLVM_UNLIKELY(quickSort(&sm, 0u, len) == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
// 6. Let itemCount be the number of elements in sortedList.
auto itemCount = JSArray::getLength(sortedListArr.get(), runtime);

// 7. Let j be 0.
uint64_t j = 0;
MutableHandle<> jHandle{runtime};
MutableHandle<> valueHandle{runtime};

// 8. Repeat, while j < itemCount,
while (j < itemCount) {
jHandle = HermesValue::encodeTrustedNumberValue(j);

valueHandle = sortedListArr.get()->at(runtime, j).unboxToHV(runtime);

if (LLVM_UNLIKELY(
JSObject::putComputed_RJS(
O,
runtime,
jHandle,
valueHandle,
PropOpFlags().plusThrowOnError()) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

// 8b. Set j to j + 1.
j += 1;
}

// 10. Repeat, while j < len,
while (j < len) {
jHandle = HermesValue::encodeTrustedNumberValue(j);

// 10a. Perform ? DeletePropertyOrThrow(obj, ! ToString(𝔽(j))).
if (LLVM_UNLIKELY(
JSObject::deleteComputed(
O, runtime, jHandle, PropOpFlags().plusThrowOnError()) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

// 10b. Set j to j + 1.
j += 1;
}

return O.getHermesValue();
}
Expand Down Expand Up @@ -3954,6 +3999,83 @@ arrayPrototypeWith(void *, Runtime &runtime, NativeArgs args) {
return A.getHermesValue();
}

/// ES14.0 23.1.3.34
CallResult<HermesValue>
arrayPrototypeToSorted(void *, Runtime &runtime, NativeArgs args) {
GCScope gcScope{runtime};

// 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw
// a TypeError exception.
auto compareFn = Handle<Callable>::dyn_vmcast(args.getArgHandle(0));
if (!args.getArg(0).isUndefined() && !compareFn) {
return runtime.raiseTypeError("Array toSorted argument must be callable");
}

// 2. Let O be ? ToObject(this value).
auto oRes = toObject(runtime, args.getThisHandle());
if (LLVM_UNLIKELY(oRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
auto O = runtime.makeHandle<JSObject>(*oRes);

// 3. Let len be ? LengthOfArrayLike(O).
Handle<JSArray> jsArr = Handle<JSArray>::dyn_vmcast(O);
auto lenRes = lengthOfArrayLike(runtime, O, jsArr);
if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
uint64_t len = lenRes.getValue();

// 4. Let A be ArrayCreate(len).
auto ARes = JSArray::create(runtime, len, len);
if (LLVM_UNLIKELY(ARes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
auto A = ARes.getValue();

// 6. Let sortedList be ? SortIndexedProperties(obj, len, SortCompare,
// skip-holes).
auto sortedListRes = sortIndexedProperties(runtime, O, len, compareFn, false);
if (LLVM_UNLIKELY(sortedListRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

Handle<> sortedList = runtime.makeHandle(*sortedListRes);
auto sortedListArr = Handle<JSArray>::dyn_vmcast(sortedList);

// 6. Let itemCount be the number of elements in sortedList.
auto itemCount = JSArray::getLength(sortedListArr.get(), runtime);

// 7. Let j be 0.
uint64_t j = 0;
MutableHandle<> jHandle{runtime};
MutableHandle<> valueHandle{runtime};

// 8. Repeat, while j < itemCount,
while (j < itemCount) {
jHandle = HermesValue::encodeTrustedNumberValue(j);

valueHandle = sortedListArr.get()->at(runtime, j).unboxToHV(runtime);

if (LLVM_UNLIKELY(
JSObject::defineOwnComputed(
A,
runtime,
jHandle,
DefinePropertyFlags::getDefaultNewPropertyFlags(),
valueHandle,
PropOpFlags().plusThrowOnError()) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

// 8b. Set j to j + 1.
j += 1;
}

return A.getHermesValue();
}

CallResult<HermesValue> arrayOf(void *, Runtime &runtime, NativeArgs args) {
GCScope gcScope{runtime};

Expand Down
Loading
Loading