LibJS: Crudely implement growable SharedArrayBuffers
We treat any mention of [[ArrayBufferByteLengthData]] and related atomic operations as FIXMEs to be fixed at a later date. We also add the HostGrowSharedArrayBuffer abstract operation, which will be overridden by LibWeb to grow shared WebAssembly memories.
This commit is contained in:
parent
ee141dd9f7
commit
d44b239621
11 changed files with 180 additions and 17 deletions
|
|
@ -315,19 +315,56 @@ ThrowCompletionOr<Optional<size_t>> get_array_buffer_max_byte_length_option(VM&
|
|||
}
|
||||
|
||||
// 25.2.2.1 AllocateSharedArrayBuffer ( constructor, byteLength [ , maxByteLength ] ), https://tc39.es/ecma262/#sec-allocatesharedarraybuffer
|
||||
ThrowCompletionOr<GC::Ref<ArrayBuffer>> allocate_shared_array_buffer(VM& vm, FunctionObject& constructor, size_t byte_length)
|
||||
ThrowCompletionOr<GC::Ref<ArrayBuffer>> allocate_shared_array_buffer(VM& vm, FunctionObject& constructor, size_t byte_length, Optional<size_t> const& max_byte_length)
|
||||
{
|
||||
// 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%SharedArrayBuffer.prototype%", « [[ArrayBufferData]], [[ArrayBufferByteLength]] »).
|
||||
// 1. Let slots be « [[ArrayBufferData]] ».
|
||||
|
||||
// 2. If maxByteLength is present and maxByteLength is not empty, let allocatingGrowableBuffer be true; otherwise let allocatingGrowableBuffer be false.
|
||||
auto allocating_growable_buffer = max_byte_length.has_value();
|
||||
|
||||
// 3. If allocatingGrowableBuffer is true, then
|
||||
if (allocating_growable_buffer) {
|
||||
// a. If byteLength > maxByteLength, throw a RangeError exception.
|
||||
if (byte_length > *max_byte_length)
|
||||
return vm.throw_completion<RangeError>(ErrorType::ByteLengthExceedsMaxByteLength, byte_length, *max_byte_length);
|
||||
|
||||
// b. Append [[ArrayBufferByteLengthData]] and [[ArrayBufferMaxByteLength]] to slots.
|
||||
}
|
||||
|
||||
// 4. Else,
|
||||
// a. Append [[ArrayBufferByteLength]] to slots.
|
||||
|
||||
// 5. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%SharedArrayBuffer.prototype%", slots).
|
||||
auto obj = TRY(ordinary_create_from_constructor<ArrayBuffer>(vm, constructor, &Intrinsics::shared_array_buffer_prototype, nullptr, DataBlock::Shared::Yes));
|
||||
|
||||
// 2. Let block be ? CreateSharedByteDataBlock(byteLength).
|
||||
auto block = TRY(create_shared_byte_data_block(vm, byte_length));
|
||||
// 6. If allocatingGrowableBuffer is true, let allocLength be maxByteLength; otherwise let allocLength be byteLength.
|
||||
auto alloc_length = allocating_growable_buffer ? *max_byte_length : byte_length;
|
||||
|
||||
// 3. Set obj.[[ArrayBufferData]] to block.
|
||||
// 4. Set obj.[[ArrayBufferByteLength]] to byteLength.
|
||||
// 7. Let block be ? CreateSharedByteDataBlock(allocLength).
|
||||
// AD-HOC: Instead, allocate the block with a length of byte_length, and ensure the capacity is alloc_length.
|
||||
auto block = TRY(create_shared_byte_data_block(vm, byte_length));
|
||||
block.buffer().ensure_capacity(alloc_length);
|
||||
|
||||
// 8. Set obj.[[ArrayBufferData]] to block.
|
||||
obj->set_data_block(move(block));
|
||||
|
||||
// 5. Return obj.
|
||||
// 9. If allocatingGrowableBuffer is true, then
|
||||
if (allocating_growable_buffer) {
|
||||
// a. Assert: byteLength ≤ maxByteLength.
|
||||
VERIFY(byte_length <= *max_byte_length);
|
||||
|
||||
// FIXME: b. Let byteLengthBlock be ? CreateSharedByteDataBlock(8).
|
||||
// FIXME: c. Perform SetValueInBuffer(byteLengthBlock, 0, biguint64, ℤ(byteLength), true, seq-cst).
|
||||
// FIXME: d. Set obj.[[ArrayBufferByteLengthData]] to byteLengthBlock.
|
||||
|
||||
// e. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
|
||||
obj->set_max_byte_length(*max_byte_length);
|
||||
}
|
||||
|
||||
// 10. Else,
|
||||
// a. Set obj.[[ArrayBufferByteLength]] to byteLength.
|
||||
|
||||
// 11. Return obj.
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ ThrowCompletionOr<ArrayBuffer*> array_buffer_copy_and_detach(VM&, ArrayBuffer& a
|
|||
JS_API ThrowCompletionOr<void> detach_array_buffer(VM&, ArrayBuffer& array_buffer, Optional<Value> key = {});
|
||||
ThrowCompletionOr<Optional<size_t>> get_array_buffer_max_byte_length_option(VM&, Value options);
|
||||
JS_API ThrowCompletionOr<ArrayBuffer*> clone_array_buffer(VM&, ArrayBuffer& source_buffer, size_t source_byte_offset, size_t source_length);
|
||||
JS_API ThrowCompletionOr<GC::Ref<ArrayBuffer>> allocate_shared_array_buffer(VM&, FunctionObject& constructor, size_t byte_length);
|
||||
JS_API ThrowCompletionOr<GC::Ref<ArrayBuffer>> allocate_shared_array_buffer(VM&, FunctionObject& constructor, size_t byte_length, Optional<size_t> const& max_byte_length = {});
|
||||
|
||||
// 25.1.3.2 ArrayBufferByteLength ( arrayBuffer, order ), https://tc39.es/ecma262/#sec-arraybufferbytelength
|
||||
inline size_t array_buffer_byte_length(ArrayBuffer const& array_buffer, ArrayBuffer::Order)
|
||||
|
|
|
|||
|
|
@ -250,6 +250,8 @@ namespace JS {
|
|||
P(groupCollapsed) \
|
||||
P(groupEnd) \
|
||||
P(groups) \
|
||||
P(grow) \
|
||||
P(growable) \
|
||||
P(has) \
|
||||
P(hasIndices) \
|
||||
P(hasOwn) \
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
M(BindingNotInitialized, "Binding {} is not initialized") \
|
||||
M(BufferOutOfBounds, "{} contains a property which references a value at an index not contained within its buffer's bounds") \
|
||||
M(ByteLengthExceedsMaxByteLength, "ArrayBuffer byte length of {} exceeds the max byte length of {}") \
|
||||
M(ByteLengthLessThanPreviousByteLength, "SharedArrayBuffer byte length of {} is less than the previous byte length of {}") \
|
||||
M(CallStackSizeExceeded, "Call stack size limit exceeded") \
|
||||
M(CannotBeHeldWeakly, "{} cannot be held weakly") \
|
||||
M(CannotDeclareGlobalFunction, "Cannot declare global function of name '{}'") \
|
||||
|
|
|
|||
|
|
@ -49,8 +49,11 @@ ThrowCompletionOr<GC::Ref<Object>> SharedArrayBufferConstructor::construct(Funct
|
|||
{
|
||||
auto& vm = this->vm();
|
||||
|
||||
auto length = vm.argument(0);
|
||||
auto options = vm.argument(1);
|
||||
|
||||
// 2. Let byteLength be ? ToIndex(length).
|
||||
auto byte_length_or_error = vm.argument(0).to_index(vm);
|
||||
auto byte_length_or_error = length.to_index(vm);
|
||||
|
||||
if (byte_length_or_error.is_error()) {
|
||||
auto error = byte_length_or_error.release_error();
|
||||
|
|
@ -61,8 +64,11 @@ ThrowCompletionOr<GC::Ref<Object>> SharedArrayBufferConstructor::construct(Funct
|
|||
return error;
|
||||
}
|
||||
|
||||
// 3. Return ? AllocateSharedArrayBuffer(NewTarget, byteLength).
|
||||
return *TRY(allocate_shared_array_buffer(vm, new_target, byte_length_or_error.release_value()));
|
||||
// 3. Let requestedMaxByteLength be ? GetArrayBufferMaxByteLengthOption(options).
|
||||
auto requested_max_byte_length = TRY(get_array_buffer_max_byte_length_option(vm, options));
|
||||
|
||||
// 4. Return ? AllocateSharedArrayBuffer(NewTarget, byteLength, requestedMaxByteLength).
|
||||
return *TRY(allocate_shared_array_buffer(vm, new_target, byte_length_or_error.release_value(), requested_max_byte_length));
|
||||
}
|
||||
|
||||
// 25.2.4.2 get SharedArrayBuffer [ @@species ], https://tc39.es/ecma262/#sec-sharedarraybuffer-@@species
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ void SharedArrayBufferPrototype::initialize(Realm& realm)
|
|||
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.grow, grow, 1, attr);
|
||||
define_native_accessor(realm, vm.names.growable, growable_getter, {}, Attribute::Configurable);
|
||||
define_native_accessor(realm, vm.names.maxByteLength, max_byte_length, {}, Attribute::Configurable);
|
||||
define_native_function(realm, vm.names.slice, slice, 2, attr);
|
||||
|
||||
// 25.2.5.7 SharedArrayBuffer.prototype [ @@toStringTag ], https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.toString
|
||||
|
|
@ -48,6 +51,101 @@ JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::byte_length_getter)
|
|||
return Value(array_buffer_object->byte_length());
|
||||
}
|
||||
|
||||
// 25.2.5.3 SharedArrayBuffer.prototype.grow ( newLength ), https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.grow
|
||||
JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::grow)
|
||||
{
|
||||
auto new_length = vm.argument(0);
|
||||
|
||||
// 1. Let O be the this value.
|
||||
auto array_buffer_object = TRY(typed_this_value(vm));
|
||||
|
||||
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]).
|
||||
if (array_buffer_object->is_fixed_length())
|
||||
return vm.throw_completion<TypeError>(ErrorType::FixedArrayBuffer);
|
||||
|
||||
// 3. If IsSharedArrayBuffer(O) is false, throw a TypeError exception.
|
||||
if (!array_buffer_object->is_shared_array_buffer())
|
||||
return vm.throw_completion<TypeError>(ErrorType::NotASharedArrayBuffer);
|
||||
|
||||
// 4. Let newByteLength be ? ToIndex(newLength).
|
||||
auto new_byte_length = TRY(new_length.to_index(vm));
|
||||
|
||||
// 5. Let hostHandled be ? HostGrowSharedArrayBuffer(O, newByteLength).
|
||||
auto host_handled = TRY(vm.host_grow_shared_array_buffer(array_buffer_object, new_byte_length));
|
||||
|
||||
// 6. If hostHandled is handled, return undefined.
|
||||
if (host_handled == HandledByHost::Handled)
|
||||
return js_undefined();
|
||||
|
||||
// FIXME: 7. Let AR be the Agent Record of the surrounding agent.
|
||||
// FIXME: 8. Let isLittleEndian be AR.[[LittleEndian]].
|
||||
// FIXME: 9. Let byteLengthBlock be O.[[ArrayBufferByteLengthData]].
|
||||
// FIXME: 10. Let currentByteLengthRawBytes be GetRawBytesFromSharedBlock(byteLengthBlock, 0, biguint64, true, seq-cst).
|
||||
// FIXME: 11. Let newByteLengthRawBytes be NumericToRawBytes(biguint64, ℤ(newByteLength), isLittleEndian).
|
||||
// FIXME: 12. Repeat,
|
||||
// FIXME: a. NOTE: This is a compare-and-exchange loop to ensure that parallel, racing grows of the same buffer are totally ordered, are not lost, and do not silently do nothing. The loop exits if it was able to attempt to grow uncontended.
|
||||
// FIXME: b. Let currentByteLength be ℝ(RawBytesToNumeric(biguint64, currentByteLengthRawBytes, isLittleEndian)).
|
||||
auto current_byte_length = array_buffer_object->byte_length();
|
||||
|
||||
// c. If newByteLength = currentByteLength, return undefined.
|
||||
if (new_byte_length == current_byte_length)
|
||||
return js_undefined();
|
||||
|
||||
// d. If newByteLength < currentByteLength or newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception.
|
||||
if (new_byte_length < current_byte_length)
|
||||
return vm.throw_completion<RangeError>(ErrorType::ByteLengthLessThanPreviousByteLength, new_byte_length, current_byte_length);
|
||||
if (new_byte_length > array_buffer_object->max_byte_length())
|
||||
return vm.throw_completion<RangeError>(ErrorType::ByteLengthExceedsMaxByteLength, new_byte_length, array_buffer_object->max_byte_length());
|
||||
|
||||
// FIXME: e. Let byteLengthDelta be newByteLength - currentByteLength.
|
||||
// FIXME: f. If it is impossible to create a new Shared Data Block value consisting of byteLengthDelta bytes, throw a RangeError exception.
|
||||
// FIXME: g. NOTE: No new Shared Data Block is constructed and used here. The observable behaviour of growable SharedArrayBuffers is specified by allocating a max-sized Shared Data Block at construction time, and this step captures the requirement that implementations that run out of memory must throw a RangeError.
|
||||
// FIXME: h. Let readByteLengthRawBytes be AtomicCompareExchangeInSharedBlock(byteLengthBlock, 0, 8, currentByteLengthRawBytes, newByteLengthRawBytes).
|
||||
// FIXME: i. If ByteListEqual(readByteLengthRawBytes, currentByteLengthRawBytes) is true, return undefined.
|
||||
// FIXME: j. Set currentByteLengthRawBytes to readByteLengthRawBytes.
|
||||
|
||||
if (auto result = array_buffer_object->buffer().try_resize(new_byte_length, ByteBuffer::ZeroFillNewElements::Yes); result.is_error())
|
||||
return vm.throw_completion<RangeError>(ErrorType::NotEnoughMemoryToAllocate, new_byte_length);
|
||||
|
||||
return js_undefined();
|
||||
}
|
||||
|
||||
// 25.2.5.4 get SharedArrayBuffer.prototype.growable, https://tc39.es/ecma262/#sec-get-sharedarraybuffer.prototype.growable
|
||||
JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::growable_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 false, throw a TypeError exception.
|
||||
if (!array_buffer_object->is_shared_array_buffer())
|
||||
return vm.throw_completion<TypeError>(ErrorType::NotASharedArrayBuffer);
|
||||
|
||||
// 4. If IsFixedLengthArrayBuffer(O) is false, return true; otherwise return false.
|
||||
return Value { !array_buffer_object->is_fixed_length() };
|
||||
}
|
||||
|
||||
// 25.2.5.5 get SharedArrayBuffer.prototype.maxByteLength, https://tc39.es/ecma262/#sec-get-sharedarraybuffer.prototype.maxbytelength
|
||||
JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::max_byte_length)
|
||||
{
|
||||
// 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.
|
||||
if (!array_buffer_object->is_shared_array_buffer())
|
||||
return vm.throw_completion<TypeError>(ErrorType::NotASharedArrayBuffer);
|
||||
|
||||
// 4. If IsFixedLengthArrayBuffer(O) is true, then
|
||||
// a. Let length be O.[[ArrayBufferByteLength]].
|
||||
// 5. Else,
|
||||
// a. Let length be O.[[ArrayBufferMaxByteLength]].
|
||||
auto length = array_buffer_object->is_fixed_length() ? array_buffer_object->byte_length() : array_buffer_object->max_byte_length();
|
||||
|
||||
// 6. Return 𝔽(length).
|
||||
return Value { length };
|
||||
}
|
||||
|
||||
// 25.2.5.6 SharedArrayBuffer.prototype.slice ( start, end ), https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.slice
|
||||
JS_DEFINE_NATIVE_FUNCTION(SharedArrayBufferPrototype::slice)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ private:
|
|||
explicit SharedArrayBufferPrototype(Realm&);
|
||||
|
||||
JS_DECLARE_NATIVE_FUNCTION(byte_length_getter);
|
||||
JS_DECLARE_NATIVE_FUNCTION(grow);
|
||||
JS_DECLARE_NATIVE_FUNCTION(growable_getter);
|
||||
JS_DECLARE_NATIVE_FUNCTION(max_byte_length);
|
||||
JS_DECLARE_NATIVE_FUNCTION(slice);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,21 @@ VM::VM(ErrorMessages error_messages)
|
|||
return HandledByHost::Handled;
|
||||
};
|
||||
|
||||
// 25.2.2.4 HostGrowSharedArrayBuffer ( buffer, newByteLength ), https://tc39.es/ecma262/#sec-hostgrowsharedarraybuffer
|
||||
host_grow_shared_array_buffer = [](ArrayBuffer&, size_t) -> ThrowCompletionOr<HandledByHost> {
|
||||
// The host-defined abstract operation HostGrowSharedArrayBuffer takes arguments buffer (a SharedArrayBuffer)
|
||||
// and newByteLength (a non-negative integer) and returns either a normal completion containing either handled
|
||||
// or unhandled, or a throw completion. It gives the host an opportunity to perform implementation-defined
|
||||
// growing of buffer. If the host chooses not to handle growing of buffer, it may return unhandled for the default behaviour.
|
||||
|
||||
// The implementation of HostGrowSharedArrayBuffer must conform to the following requirements:
|
||||
// - If the abstract operation does not complete normally with unhandled, and newByteLength < the current byte length of the buffer or newByteLength > buffer.[[ArrayBufferMaxByteLength]], throw a RangeError exception.
|
||||
// - Let AR be the Agent Record of the surrounding agent. Let isLittleEndian be AR.[[LittleEndian]]. If the abstract operation completes normally with handled, a WriteSharedMemory or ReadModifyWriteSharedMemory event whose [[Order]] is seq-cst, [[Payload]] is NumericToRawBytes(biguint64, newByteLength, isLittleEndian), [[Block]] is buffer.[[ArrayBufferByteLengthData]], [[ByteIndex]] is 0, and [[ElementSize]] is 8 is added to the surrounding agent's candidate execution such that racing calls to SharedArrayBuffer.prototype.grow ( newLength ) are not "lost", i.e. silently do nothing.
|
||||
|
||||
// The default implementation of HostGrowSharedArrayBuffer is to return NormalCompletion(unhandled).
|
||||
return HandledByHost::Unhandled;
|
||||
};
|
||||
|
||||
// 3.6.1 HostInitializeShadowRealm ( realm, context, O ), https://tc39.es/proposal-shadowrealm/#sec-hostinitializeshadowrealm
|
||||
host_initialize_shadow_realm = [](Realm&, NonnullOwnPtr<ExecutionContext>, ShadowRealm&) -> ThrowCompletionOr<void> {
|
||||
// The host-defined abstract operation HostInitializeShadowRealm takes arguments realm (a Realm Record),
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ public:
|
|||
Function<ThrowCompletionOr<void>(Realm&, ReadonlySpan<String>, StringView, StringView, CompilationType, ReadonlySpan<Value>, Value)> host_ensure_can_compile_strings;
|
||||
Function<ThrowCompletionOr<void>(Object&)> host_ensure_can_add_private_element;
|
||||
Function<ThrowCompletionOr<HandledByHost>(ArrayBuffer&, size_t)> host_resize_array_buffer;
|
||||
Function<ThrowCompletionOr<HandledByHost>(ArrayBuffer&, size_t)> host_grow_shared_array_buffer;
|
||||
Function<void(StringView)> host_unrecognized_date_string;
|
||||
Function<ThrowCompletionOr<void>(Realm&, NonnullOwnPtr<ExecutionContext>, ShadowRealm&)> host_initialize_shadow_realm;
|
||||
Function<Crypto::SignedBigInteger(Object const& global)> host_system_utc_epoch_nanoseconds;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ Harness status: OK
|
|||
|
||||
Found 150 tests
|
||||
|
||||
147 Pass
|
||||
3 Fail
|
||||
148 Pass
|
||||
2 Fail
|
||||
Pass primitive undefined
|
||||
Pass primitive null
|
||||
Pass primitive true
|
||||
|
|
@ -137,7 +137,7 @@ Pass Serializing a non-serializable platform object fails
|
|||
Pass An object whose interface is deleted from the global must still deserialize
|
||||
Pass A subclass instance will deserialize as its closest serializable superclass
|
||||
Pass Resizable ArrayBuffer
|
||||
Fail Growable SharedArrayBuffer
|
||||
Pass Growable SharedArrayBuffer
|
||||
Pass Length-tracking TypedArray
|
||||
Pass Length-tracking DataView
|
||||
Pass Serializing OOB TypedArray throws
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ Harness status: OK
|
|||
|
||||
Found 150 tests
|
||||
|
||||
147 Pass
|
||||
3 Fail
|
||||
148 Pass
|
||||
2 Fail
|
||||
Pass primitive undefined
|
||||
Pass primitive null
|
||||
Pass primitive true
|
||||
|
|
@ -137,7 +137,7 @@ Pass Serializing a non-serializable platform object fails
|
|||
Pass An object whose interface is deleted from the global must still deserialize
|
||||
Pass A subclass instance will deserialize as its closest serializable superclass
|
||||
Pass Resizable ArrayBuffer
|
||||
Fail Growable SharedArrayBuffer
|
||||
Pass Growable SharedArrayBuffer
|
||||
Pass Length-tracking TypedArray
|
||||
Pass Length-tracking DataView
|
||||
Pass Serializing OOB TypedArray throws
|
||||
|
|
|
|||
Loading…
Reference in a new issue