Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibJS: Implement the Array Grouping proposal #11412

Merged
merged 4 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Userland/Libraries/LibJS/Heap/Handle.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/Value.h>

namespace JS {

Expand Down Expand Up @@ -77,4 +78,42 @@ inline Handle<T> make_handle(T& cell)
return Handle<T>::create(&cell);
}

template<>
class Handle<Value> {
public:
Handle() = default;

static Handle create(Value value)
{
if (value.is_cell())
return Handle(value, &value.as_cell());
return Handle(value);
}

auto cell() { return m_handle.cell(); }
auto cell() const { return m_handle.cell(); }
auto value() const { return m_value; }
bool is_null() const { return m_handle.is_null(); }

private:
explicit Handle(Value value)
: m_value(value)
{
}

explicit Handle(Value value, Cell* cell)
: m_value(value)
, m_handle(Handle<Cell>::create(cell))
{
}

Value m_value;
Handle<Cell> m_handle;
};

inline Handle<Value> make_handle(Value value)
{
return Handle<Value>::create(value);
}

}
161 changes: 160 additions & 1 deletion Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Map.h>
#include <LibJS/Runtime/ObjectPrototype.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/Value.h>
Expand Down Expand Up @@ -72,6 +73,8 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
define_native_function(vm.names.keys, keys, 0, attr);
define_native_function(vm.names.entries, entries, 0, attr);
define_native_function(vm.names.copyWithin, copy_within, 2, attr);
define_native_function(vm.names.groupBy, group_by, 1, attr);
define_native_function(vm.names.groupByToMap, group_by_to_map, 1, attr);

// Use define_direct_property here instead of define_native_function so that
// Object.is(Array.prototype[Symbol.iterator], Array.prototype.values)
Expand All @@ -80,7 +83,8 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
define_direct_property(*vm.well_known_symbol_iterator(), get_without_side_effects(vm.names.values), attr);

// 23.1.3.35 Array.prototype [ @@unscopables ], https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
// With proposal, https://tc39.es/proposal-array-find-from-last/#sec-array.prototype-@@unscopables
// With find from last proposal, https://tc39.es/proposal-array-find-from-last/#sec-array.prototype-@@unscopables
// With array grouping proposal, https://tc39.es/proposal-array-grouping/#sec-array.prototype-@@unscopables
auto* unscopable_list = Object::create(global_object, nullptr);
MUST(unscopable_list->create_data_property_or_throw(vm.names.at, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.copyWithin, Value(true)));
Expand All @@ -92,6 +96,8 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
MUST(unscopable_list->create_data_property_or_throw(vm.names.findLastIndex, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.flat, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.flatMap, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.groupBy, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.groupByToMap, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.includes, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.keys, Value(true)));
MUST(unscopable_list->create_data_property_or_throw(vm.names.values, Value(true)));
Expand Down Expand Up @@ -1665,4 +1671,157 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::at)
return TRY(this_object->get(index.value()));
}

// 2.3 AddValueToKeyedGroup ( groups, key, value ), https://tc39.es/proposal-array-grouping/#sec-add-value-to-keyed-group
template<typename GroupsType, typename KeyType>
static void add_value_to_keyed_group(GlobalObject& global_object, GroupsType& groups, KeyType key, Value value)
{
// 1. For each Record { [[Key]], [[Elements]] } g of groups, do
// a. If ! SameValue(g.[[Key]], key) is true, then
// NOTE: This is performed in KeyedGroupTraits::equals for groupByToMap and Traits<JS::PropertyKey>::equals for groupBy.
auto existing_elements_iterator = groups.find(key);
if (existing_elements_iterator != groups.end()) {
// i. Assert: exactly one element of groups meets this criteria.
// NOTE: This is done on insertion into the hash map, as only `set` tells us if we overrode an entry.

// ii. Append value as the last element of g.[[Elements]].
existing_elements_iterator->value.append(value);

// iii. Return.
return;
}

// 2. Let group be the Record { [[Key]]: key, [[Elements]]: « value » }.
MarkedValueList new_elements { global_object.heap() };
new_elements.append(value);

// 3. Append group as the last element of groups.
auto result = groups.set(key, move(new_elements));
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}

// 2.1 Array.prototype.groupBy ( callbackfn [ , thisArg ] ), https://tc39.es/proposal-array-grouping/#sec-array.prototype.groupby
JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::group_by)
{
auto callback_function = vm.argument(0);
auto this_arg = vm.argument(1);

// 1. Let O be ? ToObject(this value).
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));

// 2. Let len be ? LengthOfArrayLike(O).
auto length = TRY(length_of_array_like(global_object, *this_object));

// 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
if (!callback_function.is_function())
return vm.throw_completion<TypeError>(global_object, ErrorType::NotAFunction, callback_function.to_string_without_side_effects());

// 5. Let groups be a new empty List.
OrderedHashMap<PropertyKey, MarkedValueList> groups;

// 4. Let k be 0.
// 6. Repeat, while k < len
for (size_t index = 0; index < length; ++index) {
// a. Let Pk be ! ToString(𝔽(k)).
auto index_property = PropertyKey { index };

// b. Let kValue be ? Get(O, Pk).
auto k_value = TRY(this_object->get(index_property));

// c. Let propertyKey be ? ToPropertyKey(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)).
auto property_key_value = TRY(vm.call(callback_function.as_function(), this_arg, k_value, Value(index), this_object));
auto property_key = TRY(property_key_value.to_property_key(global_object));

// d. Perform ! AddValueToKeyedGroup(groups, propertyKey, kValue).
add_value_to_keyed_group(global_object, groups, property_key, k_value);

// e. Set k to k + 1.
}

// 7. Let obj be ! OrdinaryObjectCreate(null).
auto* object = Object::create(global_object, nullptr);

// 8. For each Record { [[Key]], [[Elements]] } g of groups, do
for (auto& group : groups) {
// a. Let elements be ! CreateArrayFromList(g.[[Elements]]).
auto* elements = Array::create_from(global_object, group.value);

// b. Perform ! CreateDataPropertyOrThrow(obj, g.[[Key]], elements).
MUST(object->create_data_property_or_throw(group.key, elements));
}

// 9. Return obj.
return object;
}

// 2.2 Array.prototype.groupByToMap ( callbackfn [ , thisArg ] ), https://tc39.es/proposal-array-grouping/#sec-array.prototype.groupbymap
JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::group_by_to_map)
{
auto callback_function = vm.argument(0);
auto this_arg = vm.argument(1);

// 1. Let O be ? ToObject(this value).
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));

// 2. Let len be ? LengthOfArrayLike(O).
auto length = TRY(length_of_array_like(global_object, *this_object));

// 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
if (!callback_function.is_function())
return vm.throw_completion<TypeError>(global_object, ErrorType::NotAFunction, callback_function.to_string_without_side_effects());

struct KeyedGroupTraits : public Traits<Handle<Value>> {
static unsigned hash(Handle<Value> const& value_handle)
{
return ValueTraits::hash(value_handle.value());
}

static bool equals(Handle<Value> const& a, Handle<Value> const& b)
{
// AddValueToKeyedGroup uses SameValue on the keys on Step 1.a.
return same_value(a.value(), b.value());
}
};

// 5. Let groups be a new empty List.
OrderedHashMap<Handle<Value>, MarkedValueList, KeyedGroupTraits> groups;

// 4. Let k be 0.
// 6. Repeat, while k < len
for (size_t index = 0; index < length; ++index) {
// a. Let Pk be ! ToString(𝔽(k)).
auto index_property = PropertyKey { index };

// b. Let kValue be ? Get(O, Pk).
auto k_value = TRY(this_object->get(index_property));

// c. Let key be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
auto key = TRY(vm.call(callback_function.as_function(), this_arg, k_value, Value(index), this_object));

// d. If key is -0𝔽, set key to +0𝔽.
if (key.is_negative_zero())
key = Value(0);

// e. Perform ! AddValueToKeyedGroup(groups, key, kValue).
add_value_to_keyed_group(global_object, groups, make_handle(key), k_value);

// f. Set k to k + 1.
}

// 7. Let map be ! Construct(%Map%).
auto* map = Map::create(global_object);

// 8. For each Record { [[Key]], [[Elements]] } g of groups, do
for (auto& group : groups) {
// a. Let elements be ! CreateArrayFromList(g.[[Elements]]).
auto* elements = Array::create_from(global_object, group.value);

// b. Let entry be the Record { [[Key]]: g.[[Key]], [[Value]]: elements }.
// c. Append entry as the last element of map.[[MapData]].
map->entries().set(group.key.value(), elements);
}

// 9. Return map.
return map;
}

}
2 changes: 2 additions & 0 deletions Userland/Libraries/LibJS/Runtime/ArrayPrototype.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class ArrayPrototype final : public Array {
JS_DECLARE_NATIVE_FUNCTION(keys);
JS_DECLARE_NATIVE_FUNCTION(entries);
JS_DECLARE_NATIVE_FUNCTION(copy_within);
JS_DECLARE_NATIVE_FUNCTION(group_by);
JS_DECLARE_NATIVE_FUNCTION(group_by_to_map);
};

}
2 changes: 2 additions & 0 deletions Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ namespace JS {
P(global) \
P(globalThis) \
P(group) \
P(groupBy) \
P(groupByToMap) \
P(groupCollapsed) \
P(groupEnd) \
P(groups) \
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibJS/Runtime/ExecutionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct ExecutionContext {

[[nodiscard]] ExecutionContext copy() const
{
ExecutionContext copy { arguments.copy() };
ExecutionContext copy { arguments };

copy.function = function;
copy.realm = realm;
Expand Down
29 changes: 25 additions & 4 deletions Userland/Libraries/LibJS/Runtime/MarkedValueList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,42 @@
namespace JS {

MarkedValueList::MarkedValueList(Heap& heap)
: m_heap(heap)
: m_heap(&heap)
{
m_heap.did_create_marked_value_list({}, *this);
m_heap->did_create_marked_value_list({}, *this);
}

MarkedValueList::MarkedValueList(MarkedValueList const& other)
: Vector<Value, 32>(other)
, m_heap(other.m_heap)
{
m_heap->did_create_marked_value_list({}, *this);
}

MarkedValueList::MarkedValueList(MarkedValueList&& other)
: Vector<Value, 32>(move(static_cast<Vector<Value, 32>&>(other)))
, m_heap(other.m_heap)
{
m_heap.did_create_marked_value_list({}, *this);
m_heap->did_create_marked_value_list({}, *this);
}

MarkedValueList::~MarkedValueList()
{
m_heap.did_destroy_marked_value_list({}, *this);
m_heap->did_destroy_marked_value_list({}, *this);
}

MarkedValueList& MarkedValueList::operator=(JS::MarkedValueList const& other)
{
Vector<Value, 32>::operator=(other);

if (m_heap != other.m_heap) {
m_heap = other.m_heap;

// NOTE: IntrusiveList will remove this MarkedValueList from the old heap it was part of.
m_heap->did_create_marked_value_list({}, *this);
}

return *this;
}

}
14 changes: 3 additions & 11 deletions Userland/Libraries/LibJS/Runtime/MarkedValueList.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,18 @@
namespace JS {

class MarkedValueList : public Vector<Value, 32> {
AK_MAKE_NONCOPYABLE(MarkedValueList);

public:
explicit MarkedValueList(Heap&);
MarkedValueList(MarkedValueList const&);
MarkedValueList(MarkedValueList&&);
~MarkedValueList();

MarkedValueList& operator=(MarkedValueList&&) = delete;

Vector<Value, 32>& values() { return *this; }

MarkedValueList copy() const
{
MarkedValueList copy { m_heap };
copy.extend(*this);
return copy;
}
MarkedValueList& operator=(JS::MarkedValueList const& other);

private:
Heap& m_heap;
Heap* m_heap;

IntrusiveListNode<MarkedValueList> m_list_node;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,41 @@ describe("ability to work with generic non-array objects", () => {
2,
]);
});

test("groupBy", () => {
const visited = [];
const o = { length: 5, 0: "foo", 1: "bar", 3: "baz" };
const result = Array.prototype.groupBy.call(o, (value, _, object) => {
expect(object).toBe(o);
visited.push(value);
return value !== undefined ? value.startsWith("b") : false;
});
expect(visited).toEqual(["foo", "bar", undefined, "baz", undefined]);
expect(result.false).toEqual(["foo", undefined, undefined]);
expect(result.true).toEqual(["bar", "baz"]);
});

test("groupByToMap", () => {
const visited = [];
const o = { length: 5, 0: "foo", 1: "bar", 3: "baz" };
const falseObject = { false: false };
const trueObject = { true: true };
const result = Array.prototype.groupByToMap.call(o, (value, _, object) => {
expect(object).toBe(o);
visited.push(value);
return value !== undefined
? value.startsWith("b")
? trueObject
: falseObject
: falseObject;
});
expect(visited).toEqual(["foo", "bar", undefined, "baz", undefined]);
expect(result).toBeInstanceOf(Map);

const falseResult = result.get(falseObject);
expect(falseResult).toEqual(["foo", undefined, undefined]);

const trueResult = result.get(trueObject);
expect(trueResult).toEqual(["bar", "baz"]);
});
});
Loading