diff options
author | Idan Horowitz <idan.horowitz@gmail.com> | 2021-06-12 23:54:40 +0300 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-06-13 00:33:18 +0100 |
commit | a96ac8bd56f5a06552ed2a6fe7b21388d5454250 (patch) | |
tree | 3424f68ac39ce4676d3b38dc726c6fa89b5c40c6 /Userland/Libraries | |
parent | f9d58ec0b444e7e4ff2f4cd443f8f046e5abf9ae (diff) | |
download | serenity-a96ac8bd56f5a06552ed2a6fe7b21388d5454250.zip |
LibJS: Add the Map built-in object
Diffstat (limited to 'Userland/Libraries')
-rw-r--r-- | Userland/Libraries/LibJS/CMakeLists.txt | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Forward.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/GlobalObject.cpp | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/Map.cpp | 34 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/Map.h | 34 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/MapConstructor.cpp | 85 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/MapConstructor.h | 30 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/MapPrototype.cpp | 52 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/MapPrototype.h | 27 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/builtins/Map/Map.js | 45 |
10 files changed, 314 insertions, 0 deletions
diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 76fee7c8a7..3b2fdb5845 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -55,6 +55,9 @@ set(SOURCES Runtime/IteratorPrototype.cpp Runtime/JSONObject.cpp Runtime/LexicalEnvironment.cpp + Runtime/Map.cpp + Runtime/MapConstructor.cpp + Runtime/MapPrototype.cpp Runtime/MarkedValueList.cpp Runtime/MathObject.cpp Runtime/NativeFunction.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 1071f90645..e8350fd584 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -34,6 +34,7 @@ __JS_ENUMERATE(Date, date, DatePrototype, DateConstructor, void) \ __JS_ENUMERATE(Error, error, ErrorPrototype, ErrorConstructor, void) \ __JS_ENUMERATE(Function, function, FunctionPrototype, FunctionConstructor, void) \ + __JS_ENUMERATE(Map, map, MapPrototype, MapConstructor, void) \ __JS_ENUMERATE(NumberObject, number, NumberPrototype, NumberConstructor, void) \ __JS_ENUMERATE(Object, object, ObjectPrototype, ObjectConstructor, void) \ __JS_ENUMERATE(Promise, promise, PromisePrototype, PromiseConstructor, void) \ diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index 98bdebd4b9..688a077142 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -36,6 +36,8 @@ #include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/IteratorPrototype.h> #include <LibJS/Runtime/JSONObject.h> +#include <LibJS/Runtime/MapConstructor.h> +#include <LibJS/Runtime/MapPrototype.h> #include <LibJS/Runtime/MathObject.h> #include <LibJS/Runtime/NativeFunction.h> #include <LibJS/Runtime/NumberConstructor.h> @@ -150,6 +152,7 @@ void GlobalObject::initialize_global_object() add_constructor(vm.names.Date, m_date_constructor, m_date_prototype); add_constructor(vm.names.Error, m_error_constructor, m_error_prototype); add_constructor(vm.names.Function, m_function_constructor, m_function_prototype); + add_constructor(vm.names.Map, m_map_constructor, m_map_prototype); add_constructor(vm.names.Number, m_number_constructor, m_number_prototype); add_constructor(vm.names.Object, m_object_constructor, m_object_prototype); add_constructor(vm.names.Promise, m_promise_constructor, m_promise_prototype); diff --git a/Userland/Libraries/LibJS/Runtime/Map.cpp b/Userland/Libraries/LibJS/Runtime/Map.cpp new file mode 100644 index 0000000000..a3bc8ac05a --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Map.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibJS/Runtime/Map.h> + +namespace JS { + +Map* Map::create(GlobalObject& global_object) +{ + return global_object.heap().allocate<Map>(global_object, *global_object.map_prototype()); +} + +Map::Map(Object& prototype) + : Object(prototype) +{ +} + +Map::~Map() +{ +} + +void Map::visit_edges(Cell::Visitor& visitor) +{ + Object::visit_edges(visitor); + for (auto& value : m_entries) { + visitor.visit(value.key); + visitor.visit(value.value); + } +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Map.h b/Userland/Libraries/LibJS/Runtime/Map.h new file mode 100644 index 0000000000..c154011349 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Map.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/HashMap.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Object.h> +#include <LibJS/Runtime/Value.h> + +namespace JS { + +class Map : public Object { + JS_OBJECT(Map, Object); + +public: + static Map* create(GlobalObject&); + + explicit Map(Object& prototype); + virtual ~Map() override; + + HashMap<Value, Value, ValueTraits> const& entries() const { return m_entries; }; + HashMap<Value, Value, ValueTraits>& entries() { return m_entries; }; + +private: + virtual void visit_edges(Visitor& visitor) override; + + HashMap<Value, Value, ValueTraits> m_entries; // FIXME: Replace with a HashMap that maintains a linked list of insertion order for correct iteration order +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/MapConstructor.cpp b/Userland/Libraries/LibJS/Runtime/MapConstructor.cpp new file mode 100644 index 0000000000..d216b7e5d6 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/MapConstructor.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibJS/Runtime/Error.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/IteratorOperations.h> +#include <LibJS/Runtime/Map.h> +#include <LibJS/Runtime/MapConstructor.h> + +namespace JS { + +MapConstructor::MapConstructor(GlobalObject& global_object) + : NativeFunction(vm().names.Map, *global_object.function_prototype()) +{ +} + +void MapConstructor::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + NativeFunction::initialize(global_object); + define_property(vm.names.prototype, global_object.map_prototype(), 0); + define_property(vm.names.length, Value(0), Attribute::Configurable); + + define_native_accessor(vm.well_known_symbol_species(), symbol_species_getter, {}, Attribute::Configurable); +} + +MapConstructor::~MapConstructor() +{ +} + +Value MapConstructor::call() +{ + auto& vm = this->vm(); + vm.throw_exception<TypeError>(global_object(), ErrorType::ConstructorWithoutNew, vm.names.Map); + return {}; +} + +// 24.1.1.1 Map ( [ iterable ] ), https://tc39.es/ecma262/#sec-map-iterable +Value MapConstructor::construct(Function&) +{ + auto& vm = this->vm(); + + // FIXME: Use OrdinaryCreateFromConstructor(newTarget, "%Map.prototype%") + auto* map = Map::create(global_object()); + if (vm.argument(0).is_nullish()) + return map; + + auto adder = map->get(vm.names.set); + if (vm.exception()) + return {}; + if (!adder.is_function()) { + vm.throw_exception<TypeError>(global_object(), ErrorType::NotAFunction, "'set' property of Map"); + return {}; + } + get_iterator_values(global_object(), vm.argument(0), [&](Value iterator_value) { + if (vm.exception()) + return IterationDecision::Break; + if (!iterator_value.is_object()) { + vm.throw_exception<TypeError>(global_object(), ErrorType::NotAnObject, String::formatted("Iterator value {}", iterator_value.to_string_without_side_effects())); + return IterationDecision::Break; + } + auto key = iterator_value.as_object().get(0).value_or(js_undefined()); + if (vm.exception()) + return IterationDecision::Break; + auto value = iterator_value.as_object().get(1).value_or(js_undefined()); + if (vm.exception()) + return IterationDecision::Break; + (void)vm.call(adder.as_function(), Value(map), key, value); + return vm.exception() ? IterationDecision::Break : IterationDecision::Continue; + }); + if (vm.exception()) + return {}; + return map; +} + +// 24.1.2.2 get Map [ @@species ], https://tc39.es/ecma262/#sec-get-map-@@species +JS_DEFINE_NATIVE_GETTER(MapConstructor::symbol_species_getter) +{ + return vm.this_value(global_object); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/MapConstructor.h b/Userland/Libraries/LibJS/Runtime/MapConstructor.h new file mode 100644 index 0000000000..2bec62abdb --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/MapConstructor.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibJS/Runtime/NativeFunction.h> + +namespace JS { + +class MapConstructor final : public NativeFunction { + JS_OBJECT(MapConstructor, NativeFunction); + +public: + explicit MapConstructor(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~MapConstructor() override; + + virtual Value call() override; + virtual Value construct(Function&) override; + +private: + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_GETTER(symbol_species_getter); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/MapPrototype.cpp b/Userland/Libraries/LibJS/Runtime/MapPrototype.cpp new file mode 100644 index 0000000000..dc1b8da235 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/MapPrototype.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/HashMap.h> +#include <LibJS/Runtime/MapPrototype.h> + +namespace JS { + +MapPrototype::MapPrototype(GlobalObject& global_object) + : Object(*global_object.object_prototype()) +{ +} + +void MapPrototype::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + Object::initialize(global_object); + + define_native_accessor(vm.names.size, size_getter, {}, Attribute::Configurable); + + define_property(vm.well_known_symbol_to_string_tag(), js_string(global_object.heap(), vm.names.Map), Attribute::Configurable); +} + +MapPrototype::~MapPrototype() +{ +} + +Map* MapPrototype::typed_this(VM& vm, GlobalObject& global_object) +{ + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) + return {}; + if (!is<Map>(this_object)) { + vm.throw_exception<TypeError>(global_object, ErrorType::NotA, "Map"); + return nullptr; + } + return static_cast<Map*>(this_object); +} + +// 24.1.3.10 get Map.prototype.size, https://tc39.es/ecma262/#sec-get-map.prototype.size +JS_DEFINE_NATIVE_GETTER(MapPrototype::size_getter) +{ + auto* map = typed_this(vm, global_object); + if (!map) + return {}; + return Value(map->entries().size()); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/MapPrototype.h b/Userland/Libraries/LibJS/Runtime/MapPrototype.h new file mode 100644 index 0000000000..f11a2bc6c5 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/MapPrototype.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibJS/Runtime/Map.h> + +namespace JS { + +class MapPrototype final : public Object { + JS_OBJECT(MapPrototype, Object); + +public: + MapPrototype(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~MapPrototype() override; + +private: + static Map* typed_this(VM&, GlobalObject&); + + JS_DECLARE_NATIVE_GETTER(size_getter); +}; + +} diff --git a/Userland/Libraries/LibJS/Tests/builtins/Map/Map.js b/Userland/Libraries/LibJS/Tests/builtins/Map/Map.js new file mode 100644 index 0000000000..41e681e473 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Map/Map.js @@ -0,0 +1,45 @@ +test("constructor properties", () => { + expect(Map).toHaveLength(0); + expect(Map.name).toBe("Map"); +}); + +describe("errors", () => { + test("invalid array iterators", () => { + [-100, Infinity, NaN, {}, 152n].forEach(value => { + expect(() => { + new Map(value); + }).toThrowWithMessage(TypeError, "is not iterable"); + }); + }); + test("invalid iterator entries", () => { + expect(() => { + new Map([1, 2, 3]); + }).toThrowWithMessage(TypeError, "Iterator value 1 is not an object"); + }); + test("called without new", () => { + expect(() => { + Map(); + }).toThrowWithMessage(TypeError, "Map constructor must be called with 'new'"); + }); +}); + +describe("normal behavior", () => { + test("typeof", () => { + expect(typeof new Map()).toBe("object"); + }); + + test("constructor with single entries array argument", () => { + var a = new Map([ + ["a", 0], + ["b", 1], + ["c", 2], + ]); + expect(a instanceof Map).toBeTrue(); + expect(a).toHaveSize(3); + var seen = [false, false, false]; + a.forEach(v => { + seen[v] = true; + }); + expect(seen[0] && seen[1] && seen[2]); + }); +}); |