diff --git a/include/hermes/VM/NativeFunctions.def b/include/hermes/VM/NativeFunctions.def index 3e605b6a5df..1212e5e3201 100644 --- a/include/hermes/VM/NativeFunctions.def +++ b/include/hermes/VM/NativeFunctions.def @@ -249,6 +249,7 @@ NATIVE_FUNCTION(objectIsFrozen) NATIVE_FUNCTION(objectIsSealed) NATIVE_FUNCTION(objectKeys) NATIVE_FUNCTION(objectPreventExtensions) +NATIVE_FUNCTION(objectGroupBy) NATIVE_FUNCTION(objectPrototypeDefineGetter) NATIVE_FUNCTION(objectPrototypeDefineSetter) NATIVE_FUNCTION(objectPrototypeHasOwnProperty) diff --git a/include/hermes/VM/PredefinedStrings.def b/include/hermes/VM/PredefinedStrings.def index 6242697af14..3d354bb29a4 100644 --- a/include/hermes/VM/PredefinedStrings.def +++ b/include/hermes/VM/PredefinedStrings.def @@ -73,6 +73,7 @@ STR(getOwnPropertyDescriptor, "getOwnPropertyDescriptor") STR(getOwnPropertyDescriptors, "getOwnPropertyDescriptors") STR(getOwnPropertyNames, "getOwnPropertyNames") STR(getOwnPropertySymbols, "getOwnPropertySymbols") +STR(groupBy, "groupBy") STR(seal, "seal") STR(freeze, "freeze") STR(fromEntries, "fromEntries") diff --git a/lib/VM/JSLib/Object.cpp b/lib/VM/JSLib/Object.cpp index 1b3df7af53c..27c42aeed87 100644 --- a/lib/VM/JSLib/Object.cpp +++ b/lib/VM/JSLib/Object.cpp @@ -275,6 +275,13 @@ Handle createObjectConstructor(Runtime &runtime) { ctx, objectSetPrototypeOf, 2); + defineMethod( + runtime, + cons, + Predefined::getSymbolID(Predefined::groupBy), + ctx, + objectGroupBy, + 2); return cons; } @@ -1154,6 +1161,123 @@ objectSetPrototypeOf(void *, Runtime &runtime, NativeArgs args) { return *O; } +CallResult +objectGroupBy(void *, Runtime &runtime, NativeArgs args) { + GCScope gcScope{runtime}; + + Handle<> items = args.getArgHandle(0); + Handle grouperFunc = args.dyncastArg(1); + + // 1. Perform ? RequireObjectCoercible(items). + if (LLVM_UNLIKELY(items->isNull() || items->isUndefined())) { + return runtime.raiseTypeError( + "groupBy first argument is not coercible to Object"); + } + + // 2. If IsCallable(callbackfn) is false, throw a TypeError exception. + if (LLVM_UNLIKELY(!grouperFunc)) { + return runtime.raiseTypeError( + "groupBy second argument must be callable"); + } + + // 4. Let iteratorRecord be ? GetIterator(items, sync). + auto iteratorRecordRes = getIterator(runtime, items); + if (LLVM_UNLIKELY(iteratorRecordRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + auto iteratorRecord = *iteratorRecordRes; + + // 5. Let k be 0. + size_t k = 0; + + Handle O = runtime.makeHandle(JSObject::create(runtime)); + Handle callbackThis = runtime.makeHandle(HermesValue::encodeUndefinedValue()); + + MutableHandle<> objectArrayHandle{runtime}; + MutableHandle groupKey{runtime}; + MutableHandle targetGroupArray{runtime}; + MutableHandle<> targetGroupIndex{runtime}; + MutableHandle tmpHandle{runtime}; + MutableHandle<> valueHandle{runtime}; + auto marker = gcScope.createMarker(); + + // 6. Repeat, + // Check the length of the array after every iteration, + // to allow for the fact that the length could be modified during iteration. + for (;; k++) { + CallResult> nextRes = iteratorStep(runtime, iteratorRecord); + if (LLVM_UNLIKELY(nextRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + + if (!*nextRes) { + // Done with iteration. + break; + } + + tmpHandle = vmcast(nextRes->getHermesValue()); + auto nextValueRes = JSObject::getNamed_RJS( + tmpHandle, runtime, Predefined::getSymbolID(Predefined::value)); + if (LLVM_UNLIKELY(nextValueRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + valueHandle = runtime.makeHandle(std::move(*nextValueRes)); + + // compute key for current element + auto keyRes = Callable::executeCall2(grouperFunc, runtime, callbackThis, valueHandle.getHermesValue(), HermesValue::encodeTrustedNumberValue(k)); + if (LLVM_UNLIKELY(keyRes == ExecutionStatus::EXCEPTION)) { + return iteratorCloseAndRethrow(runtime, iteratorRecord.iterator); + } + + groupKey = runtime.makeHandle(std::move(*keyRes)); + + // make it a property key + auto propertyKeyRes = toPropertyKey(runtime, groupKey); + if (LLVM_UNLIKELY(propertyKeyRes == ExecutionStatus::EXCEPTION)) { + return iteratorCloseAndRethrow(runtime, iteratorRecord.iterator); + } + + // get the group array + auto groupArrayRes = JSObject::getComputed_RJS(O, runtime, *propertyKeyRes); + if (LLVM_UNLIKELY(groupArrayRes == ExecutionStatus::EXCEPTION)) { + return iteratorCloseAndRethrow(runtime, iteratorRecord.iterator); + } + + // new group key, no array in object yet so create it + if (groupArrayRes.getValue()->isUndefined() || groupArrayRes.getValue()->isNull()) { + auto targetGroupArrayRes = JSArray::create(runtime, 0, 0); + if (LLVM_UNLIKELY(targetGroupArrayRes == ExecutionStatus::EXCEPTION)) { + return iteratorCloseAndRethrow(runtime, iteratorRecord.iterator); + } + + targetGroupArray = std::move(*targetGroupArrayRes); + + // group already created, get the array + if (LLVM_UNLIKELY(JSObject::defineOwnComputed(O, runtime, groupKey, DefinePropertyFlags().getDefaultNewPropertyFlags(), targetGroupArray, PropOpFlags().plusThrowOnError()) == ExecutionStatus::EXCEPTION)) { + return iteratorCloseAndRethrow(runtime, iteratorRecord.iterator); + } + } else { + objectArrayHandle = runtime.makeHandle(std::move(groupArrayRes.getValue())); + targetGroupArray = Handle::dyn_vmcast(objectArrayHandle); + } + + targetGroupIndex = HermesValue::encodeTrustedNumberValue(JSArray::getLength(*targetGroupArray, runtime)); + + if (LLVM_UNLIKELY( + JSObject::putComputed_RJS( + targetGroupArray, runtime, + targetGroupIndex, valueHandle, PropOpFlags().plusThrowOnError()) == + ExecutionStatus::EXCEPTION)) { + return iteratorCloseAndRethrow(runtime, iteratorRecord.iterator); + } + + gcScope.flushToMarker(marker); + } + + // 8. Return O. + return O.getHermesValue(); +} + //===----------------------------------------------------------------------===// /// Object.prototype. diff --git a/test/hermes/object-functions.js b/test/hermes/object-functions.js index a03bea55064..43ca50af0c3 100644 --- a/test/hermes/object-functions.js +++ b/test/hermes/object-functions.js @@ -573,3 +573,32 @@ function testObjectAssignModifications() { } testObjectAssignModifications() //CHECK-NEXT: {"a":10,"c":32} + +print('groupBy'); +// CHECK-LABEL: groupBy + +var obj = Object.groupBy([1, 2, 3, 4, 5, 6, 7], function(key) { return key % 2; }); +print(JSON.stringify(obj)); +//CHECK-NEXT: {"0":[2,4,6],"1":[1,3,5,7]} + +var obj = Object.groupBy([1, 2, 3, 4, 5, 6, 7], function(key) { return key % 3; }); +print(JSON.stringify(obj)); +//CHECK-NEXT: {"0":[3,6],"1":[1,4,7],"2":[2,5]} + +var obj = Object.groupBy([1, 2, 3, 4, 5, 6, 7], function(key) { return key % 1; }); +print(JSON.stringify(obj)); +//CHECK-NEXT: {"0":[1,2,3,4,5,6,7]} + +var obj = Object.groupBy([1, 2, 3, 4], function(key) { return key; }); +print(JSON.stringify(obj)); +//CHECK-NEXT: {"1":[1],"2":[2],"3":[3],"4":[4]} + +var team = [ + {name: 'Peter', age: 15}, + {name: 'Mike', age: 20}, + {name: 'John', age: 22}, +]; + +var obj = Object.groupBy(team, function(p) { return p.age < 18 ? 'underage' : 'adult'; }); +print(JSON.stringify(obj)); +//CHECK-NEXT: {"underage":[{"name":"Peter","age":15}],"adult":[{"name":"Mike","age":20},{"name":"John","age":22}]}