1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 12:47:35 +00:00

LibJS: Implement Sets using Maps

This implements ordered sets using Maps with a sentinel value, and
includes some extra set tests.
Fixes #11004.

Co-Authored-By: davidot <davidot@serenityos.org>
This commit is contained in:
Ali Mohammad Pur 2022-02-09 18:34:16 +03:30 committed by Linus Groh
parent 4a73ec07c5
commit 3bfcd7b52d
8 changed files with 133 additions and 21 deletions

View file

@ -15,6 +15,7 @@ Set* Set::create(GlobalObject& global_object)
Set::Set(Object& prototype) Set::Set(Object& prototype)
: Object(prototype) : Object(prototype)
, m_values(*prototype.global_object().map_prototype())
{ {
} }
@ -25,8 +26,7 @@ Set::~Set()
void Set::visit_edges(Cell::Visitor& visitor) void Set::visit_edges(Cell::Visitor& visitor)
{ {
Base::visit_edges(visitor); Base::visit_edges(visitor);
for (auto& value : m_values) static_cast<Object&>(m_values).visit_edges(visitor);
visitor.visit(value);
} }
} }

View file

@ -6,8 +6,8 @@
#pragma once #pragma once
#include <AK/HashTable.h>
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Map.h>
#include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/Value.h> #include <LibJS/Runtime/Value.h>
@ -22,13 +22,24 @@ public:
explicit Set(Object& prototype); explicit Set(Object& prototype);
virtual ~Set() override; virtual ~Set() override;
OrderedHashTable<Value, ValueTraits> const& values() const { return m_values; }; // NOTE: Unlike what the spec says, we implement Sets using an underlying map,
OrderedHashTable<Value, ValueTraits>& values() { return m_values; }; // so all the functions below do not directly implement the operations as
// defined by the specification.
void set_clear() { m_values.map_clear(); }
bool set_remove(Value const& value) { return m_values.map_remove(value); }
bool set_has(Value const& key) const { return m_values.map_has(key); }
void set_add(Value const& key) { m_values.map_set(key, js_undefined()); }
size_t set_size() const { return m_values.map_size(); }
auto begin() const { return m_values.begin(); }
auto begin() { return m_values.begin(); }
auto end() const { return m_values.end(); }
private: private:
virtual void visit_edges(Visitor& visitor) override; virtual void visit_edges(Visitor& visitor) override;
OrderedHashTable<Value, ValueTraits> m_values; Map m_values;
}; };
} }

View file

@ -18,7 +18,7 @@ SetIterator::SetIterator(Set& set, Object::PropertyKind iteration_kind, Object&
: Object(prototype) : Object(prototype)
, m_set(set) , m_set(set)
, m_iteration_kind(iteration_kind) , m_iteration_kind(iteration_kind)
, m_iterator(set.values().begin()) , m_iterator(static_cast<Set const&>(set).begin())
{ {
} }

View file

@ -33,7 +33,7 @@ private:
Set& m_set; Set& m_set;
bool m_done { false }; bool m_done { false };
Object::PropertyKind m_iteration_kind; Object::PropertyKind m_iteration_kind;
OrderedHashTable<Value, ValueTraits>::Iterator m_iterator; Map::ConstIterator m_iterator;
}; };
} }

View file

@ -41,7 +41,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetIteratorPrototype::next)
return create_iterator_result_object(global_object, js_undefined(), true); return create_iterator_result_object(global_object, js_undefined(), true);
auto& set = set_iterator->set(); auto& set = set_iterator->set();
if (set_iterator->m_iterator == set.values().end()) { if (set_iterator->m_iterator == set.end()) {
set_iterator->m_done = true; set_iterator->m_done = true;
return create_iterator_result_object(global_object, js_undefined(), true); return create_iterator_result_object(global_object, js_undefined(), true);
} }
@ -49,7 +49,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetIteratorPrototype::next)
auto iteration_kind = set_iterator->iteration_kind(); auto iteration_kind = set_iterator->iteration_kind();
VERIFY(iteration_kind != Object::PropertyKind::Key); VERIFY(iteration_kind != Object::PropertyKind::Key);
auto value = *set_iterator->m_iterator; auto value = (*set_iterator->m_iterator).key;
++set_iterator->m_iterator; ++set_iterator->m_iterator;
if (iteration_kind == Object::PropertyKind::Value) if (iteration_kind == Object::PropertyKind::Value)
return create_iterator_result_object(global_object, value, false); return create_iterator_result_object(global_object, value, false);

View file

@ -52,7 +52,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetPrototype::add)
auto value = vm.argument(0); auto value = vm.argument(0);
if (value.is_negative_zero()) if (value.is_negative_zero())
value = Value(0); value = Value(0);
set->values().set(value, AK::HashSetExistingEntryBehavior::Keep); set->set_add(value);
return set; return set;
} }
@ -60,7 +60,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetPrototype::add)
JS_DEFINE_NATIVE_FUNCTION(SetPrototype::clear) JS_DEFINE_NATIVE_FUNCTION(SetPrototype::clear)
{ {
auto* set = TRY(typed_this_object(global_object)); auto* set = TRY(typed_this_object(global_object));
set->values().clear(); set->set_clear();
return js_undefined(); return js_undefined();
} }
@ -68,7 +68,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetPrototype::clear)
JS_DEFINE_NATIVE_FUNCTION(SetPrototype::delete_) JS_DEFINE_NATIVE_FUNCTION(SetPrototype::delete_)
{ {
auto* set = TRY(typed_this_object(global_object)); auto* set = TRY(typed_this_object(global_object));
return Value(set->values().remove(vm.argument(0))); return Value(set->set_remove(vm.argument(0)));
} }
// 24.2.3.5 Set.prototype.entries ( ), https://tc39.es/ecma262/#sec-set.prototype.entries // 24.2.3.5 Set.prototype.entries ( ), https://tc39.es/ecma262/#sec-set.prototype.entries
@ -86,8 +86,8 @@ JS_DEFINE_NATIVE_FUNCTION(SetPrototype::for_each)
if (!vm.argument(0).is_function()) if (!vm.argument(0).is_function())
return vm.throw_completion<TypeError>(global_object, ErrorType::NotAFunction, vm.argument(0).to_string_without_side_effects()); return vm.throw_completion<TypeError>(global_object, ErrorType::NotAFunction, vm.argument(0).to_string_without_side_effects());
auto this_value = vm.this_value(global_object); auto this_value = vm.this_value(global_object);
for (auto& value : set->values()) for (auto& entry : *set)
TRY(call(global_object, vm.argument(0).as_function(), vm.argument(1), value, value, this_value)); TRY(call(global_object, vm.argument(0).as_function(), vm.argument(1), entry.key, entry.key, this_value));
return js_undefined(); return js_undefined();
} }
@ -95,8 +95,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetPrototype::for_each)
JS_DEFINE_NATIVE_FUNCTION(SetPrototype::has) JS_DEFINE_NATIVE_FUNCTION(SetPrototype::has)
{ {
auto* set = TRY(typed_this_object(global_object)); auto* set = TRY(typed_this_object(global_object));
auto& values = set->values(); return Value(set->set_has(vm.argument(0)));
return Value(values.find(vm.argument(0)) != values.end());
} }
// 24.2.3.10 Set.prototype.values ( ), https://tc39.es/ecma262/#sec-set.prototype.values // 24.2.3.10 Set.prototype.values ( ), https://tc39.es/ecma262/#sec-set.prototype.values
@ -111,7 +110,7 @@ JS_DEFINE_NATIVE_FUNCTION(SetPrototype::values)
JS_DEFINE_NATIVE_FUNCTION(SetPrototype::size_getter) JS_DEFINE_NATIVE_FUNCTION(SetPrototype::size_getter)
{ {
auto* set = TRY(typed_this_object(global_object)); auto* set = TRY(typed_this_object(global_object));
return Value(set->values().size()); return Value(set->set_size());
} }
} }

View file

@ -46,3 +46,106 @@ describe("normal behavior", () => {
}); });
}); });
}); });
describe("modification during iteration", () => {
test("adding items during forEach also get visited", () => {
const set = new Set([1, 2]);
const visited = [];
set.forEach(val => {
if (val <= 2) set.add(4 * val);
visited.push(val);
});
expect(set).toHaveSize(4);
expect(visited).toEqual([1, 2, 4, 8]);
});
test("removing an item before it is visited means it doesn't get visited", () => {
const set = new Set([1, 2, 3]);
const visited = [];
set.forEach(val => {
visited.push(val);
if (val === 1) {
expect(set.delete(2)).toBeTrue();
} else {
expect(val).toBe(3);
expect(set.delete(2)).toBeFalse();
}
});
expect(set).toHaveSize(2);
expect(visited).toEqual([1, 3]);
});
test("removing an item after it was visited and adding it again means it gets visited twice", () => {
const set = new Set([1, 2, 3]);
const visited = [];
set.forEach(val => {
visited.push(val);
if (val === 2) {
expect(set.delete(1)).toBeTrue();
} else if (val === 3) {
expect(set).toHaveSize(2);
set.add(1);
expect(set).toHaveSize(3);
}
});
expect(set).toHaveSize(3);
expect(visited).toEqual([1, 2, 3, 1]);
});
test("adding a new item and removing it before it gets visited means it never gets visited", () => {
const set = new Set([1, 2]);
const visited = [];
set.forEach(val => {
visited.push(val);
if (val === 1) {
set.add(3);
expect(set).toHaveSize(3);
} else if (val === 2) {
expect(set).toHaveSize(3);
expect(set.delete(3)).toBeTrue();
expect(set).toHaveSize(2);
}
expect(val).not.toBe(3);
});
expect(set).toHaveSize(2);
expect(visited).toEqual([1, 2]);
});
test("removing and adding in the same iterations", () => {
const set = new Set([1, 2, 3]);
const visited = [];
let first = true;
set.forEach(val => {
visited.push(val);
if (val === 1 && first) {
expect(set.delete(1)).toBeTrue();
set.add(1);
}
first = false;
});
expect(set).toHaveSize(3);
expect(visited).toEqual([1, 2, 3, 1]);
});
test("removing and readding the same item means it can get visited n times", () => {
let n = 3;
const set = new Set([1, 2]);
const visited = [];
set.forEach(val => {
visited.push(val);
if (n-- > 0) {
expect(set.delete(val)).toBeTrue();
set.add(val);
}
});
expect(set).toHaveSize(2);
expect(visited).toEqual([1, 2, 1, 2, 1]);
});
});

View file

@ -375,13 +375,12 @@ static void print_map(JS::Object const& object, HashTable<JS::Object*>& seen_obj
static void print_set(JS::Object const& object, HashTable<JS::Object*>& seen_objects) static void print_set(JS::Object const& object, HashTable<JS::Object*>& seen_objects)
{ {
auto& set = static_cast<JS::Set const&>(object); auto& set = static_cast<JS::Set const&>(object);
auto& values = set.values();
print_type("Set"); print_type("Set");
js_out(" {{"); js_out(" {{");
bool first = true; bool first = true;
for (auto& value : values) { for (auto& entry : set) {
print_separator(first); print_separator(first);
print_value(value, seen_objects); print_value(entry.key, seen_objects);
} }
if (!first) if (!first)
js_out(" "); js_out(" ");