From 3781948f0cbf868857725497d5fc3fe26758fdb3 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Jul 2023 16:07:26 +1200 Subject: [PATCH] LibJS: Add initial implementation for SharedArrayBuffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit None of the actual sharing is implemented yet, but this is enough for most basic functionality. Diff Tests: +260 โœ… -262 โŒ +2 ๐Ÿ’€ --- Userland/Libraries/LibJS/CMakeLists.txt | 2 + Userland/Libraries/LibJS/Forward.h | 1 + .../Libraries/LibJS/Runtime/ArrayBuffer.cpp | 17 +++ .../Libraries/LibJS/Runtime/ArrayBuffer.h | 1 + .../Libraries/LibJS/Runtime/GlobalObject.cpp | 2 + .../Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../Runtime/SharedArrayBufferConstructor.cpp | 75 ++++++++++ .../Runtime/SharedArrayBufferConstructor.h | 31 ++++ .../Runtime/SharedArrayBufferPrototype.cpp | 134 ++++++++++++++++++ .../Runtime/SharedArrayBufferPrototype.h | 28 ++++ .../SharedArrayBuffer/SharedArrayBuffer.js | 19 +++ .../SharedArrayBuffer.prototype.byteLength.js | 6 + .../SharedArrayBuffer.prototype.slice.js | 29 ++++ 13 files changed, 347 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.h create mode 100644 Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.byteLength.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.slice.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 0296aab980..5a1d8a5717 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -196,6 +196,8 @@ set(SOURCES Runtime/ShadowRealmConstructor.cpp Runtime/ShadowRealmPrototype.cpp Runtime/Shape.cpp + Runtime/SharedArrayBufferConstructor.cpp + Runtime/SharedArrayBufferPrototype.cpp Runtime/StringConstructor.cpp Runtime/StringIterator.cpp Runtime/StringIteratorPrototype.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 7de394d16d..6c73f8418d 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -40,6 +40,7 @@ __JS_ENUMERATE(RegExpObject, regexp, RegExpPrototype, RegExpConstructor, void) \ __JS_ENUMERATE(Set, set, SetPrototype, SetConstructor, void) \ __JS_ENUMERATE(ShadowRealm, shadow_realm, ShadowRealmPrototype, ShadowRealmConstructor, void) \ + __JS_ENUMERATE(SharedArrayBuffer, shared_array_buffer, SharedArrayBufferPrototype, SharedArrayBufferConstructor, void) \ __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor, void) \ __JS_ENUMERATE(SuppressedError, suppressed_error, SuppressedErrorPrototype, SuppressedErrorConstructor, void) \ __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor, void) \ diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp index 4b7176389e..bd4b3c5065 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp @@ -229,4 +229,21 @@ ThrowCompletionOr array_buffer_copy_and_detach(VM& vm, ArrayBuffer return new_buffer; } +// 25.2.1.1 AllocateSharedArrayBuffer ( constructor, byteLength ), https://tc39.es/ecma262/#sec-allocatesharedarraybuffer +ThrowCompletionOr> allocate_shared_array_buffer(VM& vm, FunctionObject& constructor, size_t byte_length) +{ + // 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%SharedArrayBuffer.prototype%", ยซ [[ArrayBufferData]], [[ArrayBufferByteLength]] ยป). + auto obj = TRY(ordinary_create_from_constructor(vm, constructor, &Intrinsics::shared_array_buffer_prototype, nullptr)); + + // FIXME: 2. Let block be ? CreateSharedByteDataBlock(byteLength). + auto block = TRY(create_byte_data_block(vm, byte_length)); + + // 3. Set obj.[[ArrayBufferData]] to block. + // 4. Set obj.[[ArrayBufferByteLength]] to byteLength. + obj->set_buffer(move(block)); + + // 5. Return obj. + return obj; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h index b03735fd2a..b6050da679 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h +++ b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h @@ -104,6 +104,7 @@ ThrowCompletionOr allocate_array_buffer(VM&, FunctionObject& const ThrowCompletionOr detach_array_buffer(VM&, ArrayBuffer& array_buffer, Optional key = {}); ThrowCompletionOr clone_array_buffer(VM&, ArrayBuffer& source_buffer, size_t source_byte_offset, size_t source_length); ThrowCompletionOr array_buffer_copy_and_detach(VM&, ArrayBuffer& array_buffer, Value new_length, PreserveResizability preserve_resizability); +ThrowCompletionOr> allocate_shared_array_buffer(VM&, FunctionObject& constructor, size_t byte_length); // 25.1.2.9 RawBytesToNumeric ( type, rawBytes, isLittleEndian ), https://tc39.es/ecma262/#sec-rawbytestonumeric template diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index 88528828ae..d5c2c5f154 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -158,6 +159,7 @@ Object& set_default_global_bindings(Realm& realm) global.define_intrinsic_accessor(vm.names.RegExp, attr, [](auto& realm) -> Value { return realm.intrinsics().regexp_constructor(); }); global.define_intrinsic_accessor(vm.names.Set, attr, [](auto& realm) -> Value { return realm.intrinsics().set_constructor(); }); global.define_intrinsic_accessor(vm.names.ShadowRealm, attr, [](auto& realm) -> Value { return realm.intrinsics().shadow_realm_constructor(); }); + global.define_intrinsic_accessor(vm.names.SharedArrayBuffer, attr, [](auto& realm) -> Value { return realm.intrinsics().shared_array_buffer_constructor(); }); global.define_intrinsic_accessor(vm.names.String, attr, [](auto& realm) -> Value { return realm.intrinsics().string_constructor(); }); global.define_intrinsic_accessor(vm.names.SuppressedError, attr, [](auto& realm) -> Value { return realm.intrinsics().suppressed_error_constructor(); }); global.define_intrinsic_accessor(vm.names.Symbol, attr, [](auto& realm) -> Value { return realm.intrinsics().symbol_constructor(); }); diff --git a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp index 564b6a3e77..ef2da46459 100644 --- a/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -90,6 +90,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.cpp b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.cpp new file mode 100644 index 0000000000..eb3c7a9642 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, Shannon Booth + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +SharedArrayBufferConstructor::SharedArrayBufferConstructor(Realm& realm) + : NativeFunction(realm.vm().names.SharedArrayBuffer.as_string(), realm.intrinsics().function_prototype()) +{ +} + +ThrowCompletionOr SharedArrayBufferConstructor::initialize(Realm& realm) +{ + auto& vm = this->vm(); + MUST_OR_THROW_OOM(NativeFunction::initialize(realm)); + + // 25.2.4.2 SharedArrayBuffer.prototype, https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().shared_array_buffer_prototype(), 0); + + // 25.2.4.4 SharedArrayBuffer.prototype [ @@toStringTag ], https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.toString + define_native_accessor(realm, vm.well_known_symbol_species(), symbol_species_getter, {}, Attribute::Configurable); + + define_direct_property(vm.names.length, Value(1), Attribute::Configurable); + + return {}; +} + +// 25.2.2.1 SharedArrayBuffer ( length ), https://tc39.es/ecma262/#sec-sharedarraybuffer-length +ThrowCompletionOr SharedArrayBufferConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, vm.names.SharedArrayBuffer); +} + +// 25.2.2.1 SharedArrayBuffer ( length ), https://tc39.es/ecma262/#sec-sharedarraybuffer-length +ThrowCompletionOr> SharedArrayBufferConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + // 2. Let byteLength be ? ToIndex(length). + auto byte_length_or_error = vm.argument(0).to_index(vm); + + if (byte_length_or_error.is_error()) { + auto error = byte_length_or_error.release_error(); + if (error.value()->is_object() && is(error.value()->as_object())) { + // Re-throw more specific RangeError + return vm.throw_completion(ErrorType::InvalidLength, "shared array buffer"); + } + return error; + } + + // 3. Return ? AllocateSharedArrayBuffer(NewTarget, byteLength). + return *TRY(allocate_shared_array_buffer(vm, new_target, byte_length_or_error.release_value())); +} + +// 25.2.3.2 get SharedArrayBuffer [ @@species ], https://tc39.es/ecma262/#sec-sharedarraybuffer-@@species +JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferConstructor::symbol_species_getter) +{ + // 1. Return the this value. + return vm.this_value(); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.h b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.h new file mode 100644 index 0000000000..e0e136dc56 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferConstructor.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, Shannon Booth + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class SharedArrayBufferConstructor final : public NativeFunction { + JS_OBJECT(SharedArrayBufferConstructor, NativeFunction); + +public: + virtual ThrowCompletionOr initialize(Realm&) override; + virtual ~SharedArrayBufferConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject& new_target) override; + +private: + explicit SharedArrayBufferConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(symbol_species_getter); +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.cpp b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.cpp new file mode 100644 index 0000000000..b457274ae7 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.cpp @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023, Shannon Booth + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS { + +SharedArrayBufferPrototype::SharedArrayBufferPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().object_prototype()) +{ +} + +ThrowCompletionOr SharedArrayBufferPrototype::initialize(Realm& realm) +{ + auto& vm = this->vm(); + + MUST_OR_THROW_OOM(Base::initialize(realm)); + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_accessor(realm, vm.names.byteLength, byte_length_getter, {}, Attribute::Configurable); + define_native_function(realm, vm.names.slice, slice, 2, attr); + + // 25.2.4.4 SharedArrayBuffer.prototype [ @@toStringTag ], https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.toString + define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, vm.names.SharedArrayBuffer.as_string()), Attribute::Configurable); + + return {}; +} + +// 25.2.4.1 get SharedArrayBuffer.prototype.byteLength, https://tc39.es/ecma262/#sec-get-sharedarraybuffer.prototype.bytelength +JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::byte_length_getter) +{ + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + auto array_buffer_object = TRY(typed_this_value(vm)); + + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + // FIXME: Check for shared buffer + + // 4. Let length be O.[[ArrayBufferByteLength]]. + // 5. Return ๐”ฝ(length). + return Value(array_buffer_object->byte_length()); +} + +// 25.2.4.3 SharedArrayBuffer.prototype.slice ( start, end ), https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.slice +JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::slice) +{ + auto& realm = *vm.current_realm(); + + auto start = vm.argument(0); + auto end = vm.argument(1); + + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + auto array_buffer_object = TRY(typed_this_value(vm)); + + // 3. If IsSharedArrayBuffer(O) is false, throw a TypeError exception. + // FIXME: Check for shared buffer + + // 4. Let len be O.[[ArrayBufferByteLength]]. + auto length = array_buffer_object->byte_length(); + + // 5. Let relativeStart be ? ToIntegerOrInfinity(start). + auto relative_start = TRY(start.to_integer_or_infinity(vm)); + + double first; + // 6. If relativeStart is -โˆž, let first be 0. + if (Value(relative_start).is_negative_infinity()) + first = 0; + // 7. Else if relativeStart < 0, let first be max(len + relativeStart, 0). + else if (relative_start < 0) + first = max(length + relative_start, 0.0); + // 8. Else, let first be min(relativeStart, len). + else + first = min(relative_start, (double)length); + + // 9. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + auto relative_end = end.is_undefined() ? length : TRY(end.to_integer_or_infinity(vm)); + + double final; + // 10. If relativeEnd is -โˆž, let final be 0. + if (Value(relative_end).is_negative_infinity()) + final = 0; + // 11. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + else if (relative_end < 0) + final = max(length + relative_end, 0.0); + // 12. Else, let final be min(relativeEnd, len). + else + final = min(relative_end, (double)length); + + // 13. Let newLen be max(final - first, 0). + auto new_length = max(final - first, 0.0); + + // 14. Let ctor be ? SpeciesConstructor(O, %SharedArrayBuffer%). + auto* constructor = TRY(species_constructor(vm, array_buffer_object, realm.intrinsics().shared_array_buffer_constructor())); + + // 15. Let new be ? Construct(ctor, ยซ ๐”ฝ(newLen) ยป). + auto new_array_buffer = TRY(construct(vm, *constructor, Value(new_length))); + + // 16. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]). + if (!is(new_array_buffer.ptr())) + return vm.throw_completion(ErrorType::SpeciesConstructorDidNotCreate, "an ArrayBuffer"); + auto* new_array_buffer_object = static_cast(new_array_buffer.ptr()); + + // 17. If IsSharedArrayBuffer(new) is true, throw a TypeError exception. + // FIXME: Check for shared buffer + + // 18. If new.[[ArrayBufferData]] is O.[[ArrayBufferData]], throw a TypeError exception. + if (new_array_buffer == array_buffer_object) + return vm.throw_completion(ErrorType::SpeciesConstructorReturned, "same ArrayBuffer instance"); + + // 19. If new.[[ArrayBufferByteLength]] < newLen, throw a TypeError exception. + if (new_array_buffer_object->byte_length() < new_length) + return vm.throw_completion(ErrorType::SpeciesConstructorReturned, "an ArrayBuffer smaller than requested"); + + // 20. Let fromBuf be O.[[ArrayBufferData]]. + auto& from_buf = array_buffer_object->buffer(); + + // 21. Let toBuf be new.[[ArrayBufferData]]. + auto& to_buf = new_array_buffer_object->buffer(); + + // 22. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen). + copy_data_block_bytes(to_buf, 0, from_buf, first, new_length); + + // 23. Return new. + return new_array_buffer_object; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.h b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.h new file mode 100644 index 0000000000..5bbefda123 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/SharedArrayBufferPrototype.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, Shannon Booth + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class SharedArrayBufferPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(SharedArrayBufferPrototype, ArrayBuffer, SharedArrayBuffer); + +public: + virtual ThrowCompletionOr initialize(Realm&) override; + virtual ~SharedArrayBufferPrototype() override = default; + +private: + explicit SharedArrayBufferPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(byte_length_getter); + JS_DECLARE_NATIVE_FUNCTION(slice); +}; + +} diff --git a/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.js b/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.js new file mode 100644 index 0000000000..51464f6987 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.js @@ -0,0 +1,19 @@ +test("basic functionality", () => { + expect(SharedArrayBuffer).toHaveLength(1); + expect(SharedArrayBuffer.name).toBe("SharedArrayBuffer"); + expect(SharedArrayBuffer.prototype.constructor).toBe(SharedArrayBuffer); + expect(new SharedArrayBuffer()).toBeInstanceOf(SharedArrayBuffer); + expect(typeof new SharedArrayBuffer()).toBe("object"); +}); + +test("SharedArrayBuffer constructor must be invoked with 'new'", () => { + expect(() => { + SharedArrayBuffer(); + }).toThrowWithMessage(TypeError, "SharedArrayBuffer constructor must be called with 'new'"); +}); + +test("SharedArrayBuffer size limit", () => { + expect(() => { + new SharedArrayBuffer(2 ** 53); + }).toThrowWithMessage(RangeError, "Invalid shared array buffer length"); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.byteLength.js b/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.byteLength.js new file mode 100644 index 0000000000..a832d9425b --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.byteLength.js @@ -0,0 +1,6 @@ +test("basic functionality", () => { + expect(new SharedArrayBuffer().byteLength).toBe(0); + expect(new SharedArrayBuffer(1).byteLength).toBe(1); + expect(new SharedArrayBuffer(64).byteLength).toBe(64); + expect(new SharedArrayBuffer(123).byteLength).toBe(123); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.slice.js b/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.slice.js new file mode 100644 index 0000000000..a46cdaf9ef --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/SharedArrayBuffer/SharedArrayBuffer.prototype.slice.js @@ -0,0 +1,29 @@ +test("single parameter", () => { + const buffer = new SharedArrayBuffer(16); + const fullView = new Int32Array(buffer); + + // modify some value that we can check in the sliced buffer + fullView[3] = 7; + + // slice the buffer and use a new int32 view to perform basic checks + const slicedBuffer = buffer.slice(12); + const slicedView = new Int32Array(slicedBuffer); + + expect(slicedView).toHaveLength(1); + expect(slicedView[0]).toBe(7); +}); + +test("both parameters", () => { + const buffer = new SharedArrayBuffer(16); + const fullView = new Int32Array(buffer); + + // modify some value that we can check in the sliced buffer + fullView[1] = 12; + + // slice the buffer and use a new int32 view to perform basic checks + const slicedBuffer = buffer.slice(4, 8); + const slicedView = new Int32Array(slicedBuffer); + + expect(slicedView).toHaveLength(1); + expect(slicedView[0]).toBe(12); +});