diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index f8e359f492..5dc140c1c4 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -1,31 +1,31 @@ set(SOURCES - AST.cpp - Bytecode/ASTCodegen.cpp - Bytecode/BasicBlock.cpp - Bytecode/Generator.cpp - Bytecode/Instruction.cpp - Bytecode/Interpreter.cpp - Bytecode/Op.cpp - Console.cpp - Heap/CellAllocator.cpp - Heap/BlockAllocator.cpp - Heap/Handle.cpp - Heap/HeapBlock.cpp - Heap/Heap.cpp - Interpreter.cpp - Lexer.cpp - MarkupGenerator.cpp - Parser.cpp - Runtime/Array.cpp - Runtime/ArrayBuffer.cpp - Runtime/ArrayBufferConstructor.cpp - Runtime/ArrayBufferPrototype.cpp - Runtime/ArrayConstructor.cpp - Runtime/ArrayIterator.cpp - Runtime/ArrayIteratorPrototype.cpp - Runtime/ArrayPrototype.cpp - Runtime/BigInt.cpp - Runtime/BigIntConstructor.cpp + AST.cpp + Bytecode/ASTCodegen.cpp + Bytecode/BasicBlock.cpp + Bytecode/Generator.cpp + Bytecode/Instruction.cpp + Bytecode/Interpreter.cpp + Bytecode/Op.cpp + Console.cpp + Heap/CellAllocator.cpp + Heap/BlockAllocator.cpp + Heap/Handle.cpp + Heap/HeapBlock.cpp + Heap/Heap.cpp + Interpreter.cpp + Lexer.cpp + MarkupGenerator.cpp + Parser.cpp + Runtime/Array.cpp + Runtime/ArrayBuffer.cpp + Runtime/ArrayBufferConstructor.cpp + Runtime/ArrayBufferPrototype.cpp + Runtime/ArrayConstructor.cpp + Runtime/ArrayIterator.cpp + Runtime/ArrayIteratorPrototype.cpp + Runtime/ArrayPrototype.cpp + Runtime/BigInt.cpp + Runtime/BigIntConstructor.cpp Runtime/BigIntObject.cpp Runtime/BigIntPrototype.cpp Runtime/BooleanConstructor.cpp @@ -76,6 +76,9 @@ set(SOURCES Runtime/RegExpPrototype.cpp Runtime/ScopeObject.cpp Runtime/ScriptFunction.cpp + Runtime/Set.cpp + Runtime/SetConstructor.cpp + Runtime/SetPrototype.cpp Runtime/Shape.cpp Runtime/StringConstructor.cpp Runtime/StringIterator.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index be4536bc91..fe57b02706 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -37,6 +37,7 @@ __JS_ENUMERATE(Object, object, ObjectPrototype, ObjectConstructor, void) \ __JS_ENUMERATE(Promise, promise, PromisePrototype, PromiseConstructor, void) \ __JS_ENUMERATE(RegExpObject, regexp, RegExpPrototype, RegExpConstructor, void) \ + __JS_ENUMERATE(Set, set, SetPrototype, SetConstructor, void) \ __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor, void) \ __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor, void) diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index c78153850f..06a3e77015 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -239,6 +239,7 @@ namespace JS { P(sign) \ P(sin) \ P(sinh) \ + P(size) \ P(slice) \ P(small) \ P(some) \ diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index d0af3f8a8d..40d0461282 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -47,6 +47,8 @@ #include #include #include +#include +#include #include #include #include @@ -87,7 +89,7 @@ void GlobalObject::initialize_global_object() static_cast(m_function_prototype)->initialize(*this); static_cast(m_object_prototype)->initialize(*this); - set_prototype(m_object_prototype); + Object::set_prototype(m_object_prototype); #define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \ if (!m_##snake_name##_prototype) \ @@ -137,6 +139,7 @@ void GlobalObject::initialize_global_object() add_constructor(vm.names.Promise, m_promise_constructor, m_promise_prototype); add_constructor(vm.names.Proxy, m_proxy_constructor, nullptr); add_constructor(vm.names.RegExp, m_regexp_constructor, m_regexp_prototype); + add_constructor(vm.names.Set, m_set_constructor, m_set_prototype); add_constructor(vm.names.String, m_string_constructor, m_string_prototype); add_constructor(vm.names.Symbol, m_symbol_constructor, m_symbol_prototype); diff --git a/Userland/Libraries/LibJS/Runtime/Set.cpp b/Userland/Libraries/LibJS/Runtime/Set.cpp new file mode 100644 index 0000000000..f0cc7be803 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Set.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace JS { + +Set* Set::create(GlobalObject& global_object) +{ + return global_object.heap().allocate(global_object, *global_object.set_prototype()); +} + +Set::Set(Object& prototype) + : Object(prototype) +{ +} + +Set::~Set() +{ +} + +Set* Set::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(this_object)) { + vm.throw_exception(global_object, ErrorType::NotA, "Set"); + return nullptr; + } + return static_cast(this_object); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Set.h b/Userland/Libraries/LibJS/Runtime/Set.h new file mode 100644 index 0000000000..289d361c03 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Set.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace JS { + +struct ValueTraits : public Traits { + static unsigned hash(Value value) + { + VERIFY(!value.is_empty()); + if (value.is_string()) + return value.as_string().string().hash(); + + if (value.is_bigint()) + return value.as_bigint().big_integer().hash(); + + return u64_hash(value.encoded()); // FIXME: Is this the best way to hash pointers, doubles & ints? + } + static bool equals(const Value a, const Value b) + { + return same_value_zero(a, b); + } +}; + +class Set : public Object { + JS_OBJECT(Set, Object); + +public: + static Set* create(GlobalObject&); + + explicit Set(Object& prototype); + virtual ~Set() override; + + static Set* typed_this(VM&, GlobalObject&); + + HashTable const& values() const { return m_values; }; + HashTable& values() { return m_values; }; + +private: + HashTable m_values; // FIXME: Replace with a HashTable that maintains a linked list of insertion order for correct iteration order +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/SetConstructor.cpp b/Userland/Libraries/LibJS/Runtime/SetConstructor.cpp new file mode 100644 index 0000000000..2a3b9c2af2 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SetConstructor.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS { + +SetConstructor::SetConstructor(GlobalObject& global_object) + : NativeFunction(vm().names.Set, *global_object.function_prototype()) +{ +} + +void SetConstructor::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + NativeFunction::initialize(global_object); + define_property(vm.names.prototype, global_object.set_prototype(), 0); + define_property(vm.names.length, Value(0), Attribute::Configurable); + + define_native_property(vm.well_known_symbol_species(), symbol_species_getter, {}, Attribute::Configurable); +} + +SetConstructor::~SetConstructor() +{ +} + +Value SetConstructor::call() +{ + auto& vm = this->vm(); + vm.throw_exception(global_object(), ErrorType::ConstructorWithoutNew, vm.names.Set); + return {}; +} + +Value SetConstructor::construct(Function&) +{ + auto& vm = this->vm(); + if (vm.argument(0).is_nullish()) + return Set::create(global_object()); + + auto* set = Set::create(global_object()); + auto adder = set->get(vm.names.add); + if (vm.exception()) + return {}; + if (!adder.is_function()) { + vm.throw_exception(global_object(), ErrorType::NotAFunction, "'add' property of Set"); + return {}; + } + get_iterator_values(global_object(), vm.argument(0), [&](Value iterator_value) { + if (vm.exception()) + return IterationDecision::Break; + (void)vm.call(adder.as_function(), Value(set), iterator_value); + return vm.exception() ? IterationDecision::Break : IterationDecision::Continue; + }); + if (vm.exception()) + return {}; + return set; +} + +JS_DEFINE_NATIVE_GETTER(SetConstructor::symbol_species_getter) +{ + return vm.this_value(global_object); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SetConstructor.h b/Userland/Libraries/LibJS/Runtime/SetConstructor.h new file mode 100644 index 0000000000..68449500e9 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SetConstructor.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class SetConstructor final : public NativeFunction { + JS_OBJECT(SetConstructor, NativeFunction); + +public: + explicit SetConstructor(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~SetConstructor() 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/SetPrototype.cpp b/Userland/Libraries/LibJS/Runtime/SetPrototype.cpp new file mode 100644 index 0000000000..e2cc0c7a5d --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SetPrototype.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace JS { + +SetPrototype::SetPrototype(GlobalObject& global_object) + : Set(*global_object.object_prototype()) +{ +} + +void SetPrototype::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + Set::initialize(global_object); + u8 attr = Attribute::Writable | Attribute::Configurable; + + define_native_property(vm.names.size, size_getter, {}, attr); + + define_property(vm.well_known_symbol_to_string_tag(), js_string(global_object.heap(), vm.names.Set), Attribute::Configurable); +} + +SetPrototype::~SetPrototype() +{ +} + +JS_DEFINE_NATIVE_GETTER(SetPrototype::size_getter) +{ + auto* set = typed_this(vm, global_object); + if (!set) + return {}; + return Value(set->values().size()); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SetPrototype.h b/Userland/Libraries/LibJS/Runtime/SetPrototype.h new file mode 100644 index 0000000000..55e7e88035 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SetPrototype.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class SetPrototype final : public Set { + JS_OBJECT(SetPrototype, Set); + +public: + SetPrototype(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~SetPrototype() override; + +private: + JS_DECLARE_NATIVE_GETTER(size_getter); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/Value.h b/Userland/Libraries/LibJS/Runtime/Value.h index 2f30615489..7bdfc923d7 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.h +++ b/Userland/Libraries/LibJS/Runtime/Value.h @@ -253,6 +253,8 @@ public: i32 as_i32() const; u32 as_u32() const; + u64 encoded() const { return m_value.encoded; } + String to_string(GlobalObject&, bool legacy_null_to_empty_string = false) const; PrimitiveString* to_primitive_string(GlobalObject&); Value to_primitive(GlobalObject&, PreferredType preferred_type = PreferredType::Default) const; @@ -301,7 +303,9 @@ private: Accessor* as_accessor; BigInt* as_bigint; NativeProperty* as_native_property; - } m_value; + + u64 encoded; + } m_value { .encoded = 0 }; }; inline Value js_undefined() diff --git a/Userland/Libraries/LibJS/Tests/builtins/Set/Set.js b/Userland/Libraries/LibJS/Tests/builtins/Set/Set.js new file mode 100644 index 0000000000..2216354feb --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Set/Set.js @@ -0,0 +1,31 @@ +test("constructor properties", () => { + expect(Set).toHaveLength(0); + expect(Set.name).toBe("Set"); +}); + +describe("errors", () => { + test("invalid array iterators", () => { + [-100, Infinity, NaN, {}, 152n].forEach(value => { + expect(() => { + new Set(value); + }).toThrowWithMessage(TypeError, "is not iterable"); + }); + }); + test("called without new", () => { + expect(() => { + Set(); + }).toThrowWithMessage(TypeError, "Set constructor must be called with 'new'"); + }); +}); + +describe("normal behavior", () => { + test("typeof", () => { + expect(typeof new Set()).toBe("object"); + }); + + test("constructor with single array argument", () => { + var a = new Set([0, 1, 2]); + expect(a instanceof Set).toBeTrue(); + expect(a).toHaveSize(3); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/test-common.js b/Userland/Libraries/LibJS/Tests/test-common.js index 9a3f3869fd..b761fb3be8 100644 --- a/Userland/Libraries/LibJS/Tests/test-common.js +++ b/Userland/Libraries/LibJS/Tests/test-common.js @@ -97,6 +97,17 @@ class ExpectationError extends Error { }); } + toHaveSize(size) { + this.__expect( + typeof this.target.size === "number", + () => "toHaveSize: target.size not of type number" + ); + + this.__doMatcher(() => { + this.__expect(Object.is(this.target.size, size)); + }); + } + toHaveProperty(property, value) { this.__doMatcher(() => { let object = this.target; diff --git a/Userland/Libraries/LibWeb/Bindings/WindowObject.cpp b/Userland/Libraries/LibWeb/Bindings/WindowObject.cpp index 902e8aeebf..fd3d441432 100644 --- a/Userland/Libraries/LibWeb/Bindings/WindowObject.cpp +++ b/Userland/Libraries/LibWeb/Bindings/WindowObject.cpp @@ -43,7 +43,7 @@ void WindowObject::initialize_global_object() { Base::initialize_global_object(); - set_prototype(&ensure_web_prototype("EventTarget")); + Object::set_prototype(&ensure_web_prototype("EventTarget")); define_property("window", this, JS::Attribute::Enumerable); define_property("frames", this, JS::Attribute::Enumerable); diff --git a/Userland/Utilities/js.cpp b/Userland/Utilities/js.cpp index 29a40e9791..ee2efc19f1 100644 --- a/Userland/Utilities/js.cpp +++ b/Userland/Utilities/js.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -277,6 +278,22 @@ static void print_proxy_object(const JS::Object& object, HashTable& print_value(&proxy_object.handler(), seen_objects); } +static void print_set(const JS::Object& object, HashTable& seen_objects) +{ + auto& set = static_cast(object); + auto& values = set.values(); + print_type("Set"); + out(" {{"); + bool first = true; + for (auto& value : values) { + print_separator(first); + print_value(value, seen_objects); + } + if (!first) + out(" "); + out("}}"); +} + static void print_promise(const JS::Object& object, HashTable& seen_objects) { auto& promise = static_cast(object); @@ -398,6 +415,8 @@ static void print_value(JS::Value value, HashTable& seen_objects) return print_error(object, seen_objects); if (is(object)) return print_regexp_object(object, seen_objects); + if (is(object)) + return print_set(object, seen_objects); if (is(object)) return print_proxy_object(object, seen_objects); if (is(object))