Skip to content

Commit

Permalink
Implement ES2023 Array.prototype.toSorted (https://262.ecma-internati…
Browse files Browse the repository at this point in the history
  • Loading branch information
robik committed Feb 6, 2024
1 parent 340726e commit 86032d7
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 16 deletions.
1 change: 1 addition & 0 deletions include/hermes/VM/NativeFunctions.def
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ NATIVE_FUNCTION(arrayPrototypeSlice)
NATIVE_FUNCTION(arrayPrototypeSome)
NATIVE_FUNCTION(arrayPrototypeUnshift)
NATIVE_FUNCTION(arrayPrototypeSplice)
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 @@ -200,6 +200,7 @@ STR(includes, "includes")
STR(subarray, "subarray")
STR(flat, "flat")
STR(flatMap, "flatMap")
STR(toSorted, "toSorted")

STR(ArrayBuffer, "ArrayBuffer")
STR(byteLength, "byteLength")
Expand Down
202 changes: 186 additions & 16 deletions lib/VM/JSLib/Array.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ Handle<JSObject> createArrayConstructor(Runtime &runtime) {
(void *)IterationKind::Entry,
arrayPrototypeIterator,
0);
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 @@ -546,6 +553,28 @@ arrayPrototypeToLocaleString(void *, Runtime &runtime, NativeArgs args) {
return HermesValue::encodeStringValue(*builder->getStringPrimitive());
}

static inline CallResult<uint32_t> lengthOfArrayLike(
Runtime &runtime,
const Handle<JSObject> &O,
const Handle<JSArray> &jsArr) {
if (LLVM_LIKELY(jsArr)) {
// Fast path for getting the length.
return JSArray::getLength(jsArr.get(), runtime);
}
// Slow path
CallResult<PseudoHandle<>> propRes = JSObject::getNamed_RJS(
O, runtime, Predefined::getSymbolID(Predefined::length));
if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
auto lenRes = toLength(runtime, runtime.makeHandle(std::move(*propRes)));
if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

return lenRes->getNumber();
}

// 23.1.3.1
CallResult<HermesValue>
arrayPrototypeAt(void *, Runtime &runtime, NativeArgs args) {
Expand All @@ -559,23 +588,11 @@ arrayPrototypeAt(void *, Runtime &runtime, NativeArgs args) {

// 2. Let len be ? LengthOfArrayLike(O).
Handle<JSArray> jsArr = Handle<JSArray>::dyn_vmcast(O);
uint32_t len = 0;
if (LLVM_LIKELY(jsArr)) {
// Fast path for getting the length.
len = JSArray::getLength(jsArr.get(), runtime);
} else {
// Slow path
CallResult<PseudoHandle<>> propRes = JSObject::getNamed_RJS(
O, runtime, Predefined::getSymbolID(Predefined::length));
if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
auto lenRes = toLength(runtime, runtime.makeHandle(std::move(*propRes)));
if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
len = lenRes->getNumber();
auto lenRes = lengthOfArrayLike(runtime, O, jsArr);
if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
auto len = lenRes.getValue();

// 3. Let relativeIndex be ? ToIntegerOrInfinity(index).
auto idx = args.getArgHandle(0);
Expand Down Expand Up @@ -1421,6 +1438,119 @@ CallResult<HermesValue> sortSparse(

return O.getHermesValue();
}

// This function has copied body instead of copyFlag to reduce branching in for
// loop
CallResult<HermesValue> sortSparse(
Runtime &runtime,
Handle<JSObject> O,
Handle<JSObject> Odest,
Handle<Callable> compareFn,
uint64_t len) {
GCScope gcScope{runtime};

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

assert(
!Odest->isHostObject() && !Odest->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 Odest.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 Odest.getHermesValue();

// Create a new array which we will actually sort.
auto crArray = JSArray::create(runtime, numProps, numProps);
if (LLVM_UNLIKELY(crArray == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
auto array = *crArray;
if (LLVM_UNLIKELY(
JSArray::setStorageEndIndex(array, runtime, numProps) ==
ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}

MutableHandle<> propName{runtime};
MutableHandle<> propVal{runtime};
GCScopeMarkerRAII gcMarker{gcScope};

// Copy all sortable properties into the array.
for (decltype(numProps) i = 0; i != numProps; ++i) {
gcMarker.flush();

propName = names->at(runtime, i).unboxToHV(runtime);
auto res = JSObject::getComputed_RJS(O, runtime, propName);
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
// Skip empty values.
if (res->getHermesValue().isEmpty())
continue;

const auto shv = SmallHermesValue::encodeHermesValue(res->get(), runtime);
JSArray::unsafeSetExistingElementAt(*array, runtime, i, shv);
}
gcMarker.flush();

{
StandardSortModel sm(runtime, array, compareFn);
if (LLVM_UNLIKELY(
quickSort(&sm, 0u, numProps) == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
}

// Time to copy back the values.
for (decltype(numProps) i = 0; i != numProps; ++i) {
gcMarker.flush();

auto hv = array->at(runtime, i).unboxToHV(runtime);
assert(
!hv.isEmpty() &&
"empty values cannot appear in the array out of nowhere");
propVal = hv;

propName = HermesValue::encodeTrustedNumberValue(i);

if (JSObject::putComputed_RJS(
Odest,
runtime,
propName,
propVal,
PropOpFlags().plusThrowOnError()) == ExecutionStatus::EXCEPTION) {
return ExecutionStatus::EXCEPTION;
}
}

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

/// ES5.1 15.4.4.11.
Expand Down Expand Up @@ -3512,6 +3642,46 @@ arrayPrototypeIncludes(void *, Runtime &runtime, NativeArgs args) {
return HermesValue::encodeBoolValue(false);
}

/// 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();

// Steps 5-8

// Since we are not modifying original object, we can use sortSparse
return sortSparse(runtime, O, A, compareFn, len);
}

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

Expand Down
22 changes: 22 additions & 0 deletions test/hermes/array-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1114,3 +1114,25 @@ print(Array.prototype.at.call({length: 3, 0: 'a', 1: 'b', 2: 'c'}, -1));
// CHECK-NEXT: c
print(Array.prototype.at.call({length: 30}, 5));
// CHECK-NEXT: undefined

print('toSorted');
// CHECK-LABEL: toSorted
print(Array.prototype.toSorted.length);
// CHECK-NEXT: 1
try {
[].toSorted(1);
} catch (e) {
print(e.name)
}
// CHECK-NEXT: TypeError
var a = [ 0, 2, 1, 3 ];
print(a.toSorted().toString())
// CHECK-NEXT: 0,1,2,3
print(a.toString())
// CHECK-NEXT: 0,2,1,3
print(arrayEquals([ 'aa', 'a', 'aaa', 0 ].toSorted(), [ 0, 'a', 'aa', 'aaa' ]));
// CHECK-NEXT: true
print(arrayEquals([ 'aa', 'a', 'aaa' ].toSorted(function(a, b) { return b.length - a.length; }), [ 'aaa', 'aa', 'a' ]));
// CHECK-NEXT: true
print(Array.prototype.toSorted.call({length : 3, 0 : 'b', 1 : 'c', 2 : 'a'}).toString());
// CHECK-NEXT: a,b,c

0 comments on commit 86032d7

Please sign in to comment.