diff --git a/include/hermes/VM/NativeFunctions.def b/include/hermes/VM/NativeFunctions.def index 3e605b6a5df..3d4becb4e57 100644 --- a/include/hermes/VM/NativeFunctions.def +++ b/include/hermes/VM/NativeFunctions.def @@ -199,6 +199,7 @@ NATIVE_FUNCTION(jsonParse) NATIVE_FUNCTION(jsonStringify) NATIVE_FUNCTION(mapConstructor) NATIVE_FUNCTION(mapIteratorPrototypeNext) +NATIVE_FUNCTION(mapGroupBy) NATIVE_FUNCTION(mapPrototypeClear) NATIVE_FUNCTION(mapPrototypeDelete) NATIVE_FUNCTION(mapPrototypeEntries) 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/Map.cpp b/lib/VM/JSLib/Map.cpp index e0aa330aa0e..93716269771 100644 --- a/lib/VM/JSLib/Map.cpp +++ b/lib/VM/JSLib/Map.cpp @@ -133,6 +133,15 @@ Handle<JSObject> createMapConstructor(Runtime &runtime) { 0, CellKind::JSMapKind); + // Map.xxx static functions + defineMethod( + runtime, + cons, + Predefined::getSymbolID(Predefined::groupBy), + nullptr, + mapGroupBy, + 2); + return cons; } @@ -345,5 +354,116 @@ mapIteratorPrototypeNext(void *, Runtime &runtime, NativeArgs args) { } return *cr; } + +CallResult<HermesValue> +mapGroupBy(void *, Runtime &runtime, NativeArgs args) { + GCScope gcScope{runtime}; + + Handle<> items = args.getArgHandle(0); + Handle<Callable> grouperFunc = args.dyncastArg<Callable>(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; + + auto O = runtime.makeHandle(JSMap::create(runtime, Handle<JSObject>::vmcast(&runtime.mapPrototype))); + JSMap::initializeStorage(O, runtime); + Handle<HermesValue> callbackThis = runtime.makeHandle(HermesValue::encodeUndefinedValue()); + + MutableHandle<> objectArrayHandle{runtime}; + MutableHandle<HermesValue> groupKey{runtime}; + MutableHandle<JSArray> targetGroupArray{runtime}; + MutableHandle<> targetGroupIndex{runtime}; + MutableHandle<JSObject> 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<Handle<JSObject>> nextRes = iteratorStep(runtime, iteratorRecord); + if (LLVM_UNLIKELY(nextRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + + if (!*nextRes) { + // Done with iteration. + break; + } + + tmpHandle = vmcast<JSObject>(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); + } + auto propertyKey = *propertyKeyRes; + + // new group key, no array in object yet so create it + if (!JSMap::hasKey(O, runtime, propertyKey)) { + 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 + JSMap::addValue(O, runtime, propertyKey, targetGroupArray); + } else { + objectArrayHandle = runtime.makeHandle(std::move(JSMap::getValue(O, runtime, propertyKey))); + targetGroupArray = Handle<JSArray>::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(); +} } // namespace vm } // namespace hermes diff --git a/test/hermes/map.js b/test/hermes/map.js index ded1ce7f5bf..e8aeea787ed 100644 --- a/test/hermes/map.js +++ b/test/hermes/map.js @@ -363,3 +363,49 @@ testClear(o1, o2, o3); testIteration(); testForEach(); testZero(); + + +print("Map.groupBy"); +//CHECK-LABEL: Map.groupBy + +var map = Map.groupBy([1, 2, 3, 4, 5, 6, 7], function(key) { return key % 2; }); +print(map.get('0')) +//CHECK-NEXT: 2,4,6 +print(map.get('1')) +//CHECK-NEXT: 1,3,5,7 +print(map.size); +//CHECK-NEXT: 2 + +var map = Map.groupBy([1, 2, 3, 4, 5, 6, 7], function(key) { return key % 3; }); +print(map.get('0')) +//CHECK-NEXT: 3,6 +print(map.get('1')) +//CHECK-NEXT: 1,4,7 +print(map.get('2')) +//CHECK-NEXT: 2,5 +print(map.size); +//CHECK-NEXT: 3 + +var map = Map.groupBy([1, 2, 3, 4, 5, 6, 7], function(key) { return key % 1; }); +print(map.get('0')) +//CHECK-NEXT: 1,2,3,4,5,6,7 +print(map.size); +//CHECK-NEXT: 1 + +var map = Map.groupBy([1, 2, 3, 4], function(key) { return key; }); +print(map.size); +//CHECK-NEXT: 4 + +var team = [ + {name: 'Peter', age: 15}, + {name: 'Mike', age: 20}, + {name: 'John', age: 22}, +]; + +var map = Map.groupBy(team, function(p) { return p.age < 18 ? 'underage' : 'adult'; }); +print(map.size); +//CHECK-NEXT: 2 +print(JSON.stringify(map.get('underage'))) +//CHECK-NEXT: [{"name":"Peter","age":15}] +print(JSON.stringify(map.get('adult'))) +//CHECK-NEXT: [{"name":"Mike","age":20},{"name":"John","age":22}] \ No newline at end of file