Skip to content

Commit

Permalink
feat: implement ES2024 Object.groupBy
Browse files Browse the repository at this point in the history
  • Loading branch information
robik committed Jun 28, 2024
1 parent 3034371 commit 01bab5b
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 0 deletions.
1 change: 1 addition & 0 deletions include/hermes/VM/NativeFunctions.def
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 @@ -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")
Expand Down
124 changes: 124 additions & 0 deletions lib/VM/JSLib/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ Handle<JSObject> createObjectConstructor(Runtime &runtime) {
ctx,
objectSetPrototypeOf,
2);
defineMethod(
runtime,
cons,
Predefined::getSymbolID(Predefined::groupBy),
ctx,
objectGroupBy,
2);

return cons;
}
Expand Down Expand Up @@ -1154,6 +1161,123 @@ objectSetPrototypeOf(void *, Runtime &runtime, NativeArgs args) {
return *O;
}

CallResult<HermesValue>
objectGroupBy(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;

Handle<JSObject> O = runtime.makeHandle(JSObject::create(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);
}

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

//===----------------------------------------------------------------------===//
/// Object.prototype.

Expand Down
29 changes: 29 additions & 0 deletions test/hermes/object-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}]}

0 comments on commit 01bab5b

Please sign in to comment.