From 86032d776fe149d78e0298fec676fd5790d66c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Pasi=C5=84ski?= Date: Sat, 27 Jan 2024 12:00:24 +0100 Subject: [PATCH] Implement ES2023 Array.prototype.toSorted (https://262.ecma-international.org/14.0/#sec-array.prototype.tosorted) --- include/hermes/VM/NativeFunctions.def | 1 + include/hermes/VM/PredefinedStrings.def | 1 + lib/VM/JSLib/Array.cpp | 202 ++++++++++++++++++++++-- test/hermes/array-functions.js | 22 +++ 4 files changed, 210 insertions(+), 16 deletions(-) diff --git a/include/hermes/VM/NativeFunctions.def b/include/hermes/VM/NativeFunctions.def index e76b21d75c2..953df3a3b22 100644 --- a/include/hermes/VM/NativeFunctions.def +++ b/include/hermes/VM/NativeFunctions.def @@ -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) diff --git a/include/hermes/VM/PredefinedStrings.def b/include/hermes/VM/PredefinedStrings.def index 7c266666a5f..2c3f3ac7595 100644 --- a/include/hermes/VM/PredefinedStrings.def +++ b/include/hermes/VM/PredefinedStrings.def @@ -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") diff --git a/lib/VM/JSLib/Array.cpp b/lib/VM/JSLib/Array.cpp index 8a737170a8c..89d22fc6696 100644 --- a/lib/VM/JSLib/Array.cpp +++ b/lib/VM/JSLib/Array.cpp @@ -128,6 +128,13 @@ Handle 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))); @@ -546,6 +553,28 @@ arrayPrototypeToLocaleString(void *, Runtime &runtime, NativeArgs args) { return HermesValue::encodeStringValue(*builder->getStringPrimitive()); } +static inline CallResult lengthOfArrayLike( + Runtime &runtime, + const Handle &O, + const Handle &jsArr) { + if (LLVM_LIKELY(jsArr)) { + // Fast path for getting the length. + return JSArray::getLength(jsArr.get(), runtime); + } + // Slow path + CallResult> 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 arrayPrototypeAt(void *, Runtime &runtime, NativeArgs args) { @@ -559,23 +588,11 @@ arrayPrototypeAt(void *, Runtime &runtime, NativeArgs args) { // 2. Let len be ? LengthOfArrayLike(O). Handle jsArr = Handle::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> 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); @@ -1421,6 +1438,119 @@ CallResult sortSparse( return O.getHermesValue(); } + +// This function has copied body instead of copyFlag to reduce branching in for +// loop +CallResult sortSparse( + Runtime &runtime, + Handle O, + Handle Odest, + Handle 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. @@ -3512,6 +3642,46 @@ arrayPrototypeIncludes(void *, Runtime &runtime, NativeArgs args) { return HermesValue::encodeBoolValue(false); } +/// ES14.0 23.1.3.34 +CallResult +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::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(*oRes); + + // 3. Let len be ? LengthOfArrayLike(O). + Handle jsArr = Handle::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 arrayOf(void *, Runtime &runtime, NativeArgs args) { GCScope gcScope{runtime}; diff --git a/test/hermes/array-functions.js b/test/hermes/array-functions.js index a8dd31bcaf3..e3664abd195 100644 --- a/test/hermes/array-functions.js +++ b/test/hermes/array-functions.js @@ -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