From 78edaad97d2b8c079932c2d2373414634066a9e6 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 15 Nov 2023 16:03:27 -0500 Subject: [PATCH] LibJS: Stub out Atomics.wait and Atomics.waitAsync We don't have the facilities to implement these methods fully (namely, a fully realized SharedArrayBuffer). But we can implement enough to validate the values passed in by the user. --- .../Libraries/LibJS/Runtime/AtomicsObject.cpp | 83 +++++++++++++++++++ .../Libraries/LibJS/Runtime/AtomicsObject.h | 2 + .../LibJS/Runtime/CommonPropertyNames.h | 2 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 2 + .../Tests/builtins/Atomics/Atomics.wait.js | 76 +++++++++++++++++ .../builtins/Atomics/Atomics.waitAsync.js | 76 +++++++++++++++++ 6 files changed, 241 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.wait.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.waitAsync.js diff --git a/Userland/Libraries/LibJS/Runtime/AtomicsObject.cpp b/Userland/Libraries/LibJS/Runtime/AtomicsObject.cpp index e484d822a7..4e5031df67 100644 --- a/Userland/Libraries/LibJS/Runtime/AtomicsObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/AtomicsObject.cpp @@ -16,10 +16,12 @@ #include #include #include +#include #include #include #include #include +#include namespace JS { @@ -110,6 +112,61 @@ static ThrowCompletionOr atomic_read_modify_write(VM& vm, TypedArrayBase& return typed_array.get_modify_set_value_in_buffer(indexed_position, value_to_set, move(operation)); } +enum class WaitMode { + Sync, + Async, +}; + +// 25.4.3.14 DoWait ( mode, typedArray, index, value, timeout ), https://tc39.es/ecma262/#sec-dowait +static ThrowCompletionOr do_wait(VM& vm, WaitMode mode, TypedArrayBase& typed_array, Value index_value, Value expected_value, Value timeout_value) +{ + // 1. Let iieoRecord be ? ValidateIntegerTypedArray(typedArray, true). + // 2. Let buffer be iieoRecord.[[Object]].[[ViewedArrayBuffer]]. + // FIXME: An IIEO record is a new structure from the resizable array buffer proposal. Use it when the proposal is implemented. + auto* buffer = TRY(validate_integer_typed_array(vm, typed_array, true)); + + // 3. If IsSharedArrayBuffer(buffer) is false, throw a TypeError exception. + if (!buffer->is_shared_array_buffer()) + return vm.throw_completion(ErrorType::NotASharedArrayBuffer); + + // 4. Let i be ? ValidateAtomicAccess(iieoRecord, index). + auto index = TRY(validate_atomic_access(vm, typed_array, index_value)); + + // 5. Let arrayTypeName be typedArray.[[TypedArrayName]]. + auto const& array_type_name = typed_array.element_name(); + + // 6. If arrayTypeName is "BigInt64Array", let v be ? ToBigInt64(value). + i64 value = 0; + if (array_type_name == vm.names.BigInt64Array.as_string()) + value = TRY(expected_value.to_bigint_int64(vm)); + // 7. Else, let v be ? ToInt32(value). + else + value = TRY(expected_value.to_i32(vm)); + + // 8. Let q be ? ToNumber(timeout). + auto timeout_number = TRY(timeout_value.to_number(vm)); + + // 9. If q is either NaN or +∞𝔽, let t be +∞; else if q is -∞𝔽, let t be 0; else let t be max(ℝ(q), 0). + double timeout = 0; + if (timeout_number.is_nan() || timeout_number.is_positive_infinity()) + timeout = js_infinity().as_double(); + else if (timeout_number.is_negative_infinity()) + timeout = 0.0; + else + timeout = max(timeout_number.as_double(), 0.0); + + // 10. If mode is sync and AgentCanSuspend() is false, throw a TypeError exception. + if (mode == WaitMode::Sync && !agent_can_suspend()) + return vm.throw_completion(ErrorType::AgentCannotSuspend); + + // FIXME: Implement the remaining steps when we support SharedArrayBuffer. + (void)index; + (void)value; + (void)timeout; + + return vm.throw_completion(ErrorType::NotImplemented, "SharedArrayBuffer"sv); +} + template static ThrowCompletionOr perform_atomic_operation(VM& vm, TypedArrayBase& typed_array, AtomicFunction&& operation) { @@ -154,6 +211,8 @@ void AtomicsObject::initialize(Realm& realm) define_native_function(realm, vm.names.or_, or_, 3, attr); define_native_function(realm, vm.names.store, store, 3, attr); define_native_function(realm, vm.names.sub, sub, 3, attr); + define_native_function(realm, vm.names.wait, wait, 4, attr); + define_native_function(realm, vm.names.waitAsync, wait_async, 4, attr); define_native_function(realm, vm.names.xor_, xor_, 3, attr); // 25.4.15 Atomics [ @@toStringTag ], https://tc39.es/ecma262/#sec-atomics-@@tostringtag @@ -403,6 +462,30 @@ JS_DEFINE_NATIVE_FUNCTION(AtomicsObject::sub) VERIFY_NOT_REACHED(); } +// 25.4.13 Atomics.wait ( typedArray, index, value, timeout ), https://tc39.es/ecma262/#sec-atomics.wait +JS_DEFINE_NATIVE_FUNCTION(AtomicsObject::wait) +{ + auto* typed_array = TRY(typed_array_from(vm, vm.argument(0))); + auto index = vm.argument(1); + auto value = vm.argument(2); + auto timeout = vm.argument(3); + + // 1. Return ? DoWait(sync, typedArray, index, value, timeout). + return TRY(do_wait(vm, WaitMode::Sync, *typed_array, index, value, timeout)); +} + +// 25.4.14 Atomics.waitAsync ( typedArray, index, value, timeout ), https://tc39.es/ecma262/#sec-atomics.waitasync +JS_DEFINE_NATIVE_FUNCTION(AtomicsObject::wait_async) +{ + auto* typed_array = TRY(typed_array_from(vm, vm.argument(0))); + auto index = vm.argument(1); + auto value = vm.argument(2); + auto timeout = vm.argument(3); + + // 1. Return ? DoWait(async, typedArray, index, value, timeout). + return TRY(do_wait(vm, WaitMode::Async, *typed_array, index, value, timeout)); +} + // 25.4.14 Atomics.xor ( typedArray, index, value ), https://tc39.es/ecma262/#sec-atomics.xor JS_DEFINE_NATIVE_FUNCTION(AtomicsObject::xor_) { diff --git a/Userland/Libraries/LibJS/Runtime/AtomicsObject.h b/Userland/Libraries/LibJS/Runtime/AtomicsObject.h index 10d1763b70..004263718a 100644 --- a/Userland/Libraries/LibJS/Runtime/AtomicsObject.h +++ b/Userland/Libraries/LibJS/Runtime/AtomicsObject.h @@ -30,6 +30,8 @@ private: JS_DECLARE_NATIVE_FUNCTION(or_); JS_DECLARE_NATIVE_FUNCTION(store); JS_DECLARE_NATIVE_FUNCTION(sub); + JS_DECLARE_NATIVE_FUNCTION(wait); + JS_DECLARE_NATIVE_FUNCTION(wait_async); JS_DECLARE_NATIVE_FUNCTION(xor_); }; diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index c712a79ff6..2a8b24c8cb 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -575,6 +575,8 @@ namespace JS { P(value) \ P(valueOf) \ P(values) \ + P(wait) \ + P(waitAsync) \ P(warn) \ P(weekOfYear) \ P(weekday) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 8fbb3fce6c..b22c1fba0c 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -12,6 +12,7 @@ M(ArrayMaxSize, "Maximum array size exceeded") \ M(AccessorBadField, "Accessor descriptor's '{}' field must be a function or undefined") \ M(AccessorValueOrWritable, "Accessor property descriptor cannot specify a value or writable key") \ + M(AgentCannotSuspend, "Agent is not allowed to suspend") \ M(BigIntBadOperator, "Cannot use {} operator with BigInt") \ M(BigIntBadOperatorOtherType, "Cannot use {} operator with BigInt and other type") \ M(BigIntFromNonIntegral, "Cannot convert non-integral number to BigInt") \ @@ -94,6 +95,7 @@ M(NotAnObjectOfType, "Not an object of type {}") \ M(NotAnObjectOrNull, "{} is neither an object nor null") \ M(NotAnObjectOrString, "{} is neither an object nor a string") \ + M(NotASharedArrayBuffer, "The TypedArray's underlying buffer must be a SharedArrayBuffer") \ M(NotAString, "{} is not a string") \ M(NotASymbol, "{} is not a symbol") \ M(NotImplemented, "TODO({} is not implemented in LibJS)") \ diff --git a/Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.wait.js b/Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.wait.js new file mode 100644 index 0000000000..39378ad6c7 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.wait.js @@ -0,0 +1,76 @@ +describe("errors", () => { + test("called on non-TypedArray", () => { + expect(() => { + Atomics.wait(Symbol.hasInstance, 0, 0, 0); + }).toThrowWithMessage(TypeError, "Not an object of type TypedArray"); + }); + + test("detached buffer", () => { + expect(() => { + const typedArray = new Int32Array(4); + detachArrayBuffer(typedArray.buffer); + + Atomics.wait(typedArray, 0, 0, 0); + }).toThrowWithMessage(TypeError, "ArrayBuffer is detached"); + }); + + test("invalid TypedArray type", () => { + expect(() => { + const typedArray = new Float32Array(4); + Atomics.wait(typedArray, 0, 0, 0); + }).toThrowWithMessage( + TypeError, + "Typed array Float32Array element type is not Int32 or BigInt64" + ); + }); + + test("non-shared ArrayBuffer", () => { + expect(() => { + const typedArray = new Int32Array(4); + Atomics.wait(typedArray, 0, 0, 0); + }).toThrowWithMessage( + TypeError, + "The TypedArray's underlying buffer must be a SharedArrayBuffer" + ); + }); + + test("invalid index", () => { + expect(() => { + const buffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT); + const typedArray = new Int32Array(buffer); + + Atomics.wait(typedArray, 4, 0, 0); + }).toThrowWithMessage(RangeError, "Index 4 is out of range of array length 4"); + }); + + test("invalid value", () => { + expect(() => { + const buffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT); + const typedArray = new Int32Array(buffer); + + Atomics.wait(typedArray, 0, Symbol.hasInstance, 0); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + + expect(() => { + const buffer = new SharedArrayBuffer(4 * BigInt64Array.BYTES_PER_ELEMENT); + const typedArray = new BigInt64Array(buffer); + + Atomics.wait(typedArray, 0, Symbol.hasInstance, 0); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to BigInt"); + }); + + test("invalid timeout", () => { + expect(() => { + const buffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT); + const typedArray = new Int32Array(buffer); + + Atomics.wait(typedArray, 0, 0, Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + }); +}); + +test("basic functionality", () => { + test("invariants", () => { + expect(Atomics.wait).toHaveLength(4); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.waitAsync.js b/Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.waitAsync.js new file mode 100644 index 0000000000..cc5ebc3006 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Atomics/Atomics.waitAsync.js @@ -0,0 +1,76 @@ +describe("errors", () => { + test("called on non-TypedArray", () => { + expect(() => { + Atomics.waitAsync(Symbol.hasInstance, 0, 0, 0); + }).toThrowWithMessage(TypeError, "Not an object of type TypedArray"); + }); + + test("detached buffer", () => { + expect(() => { + const typedArray = new Int32Array(4); + detachArrayBuffer(typedArray.buffer); + + Atomics.waitAsync(typedArray, 0, 0, 0); + }).toThrowWithMessage(TypeError, "ArrayBuffer is detached"); + }); + + test("invalid TypedArray type", () => { + expect(() => { + const typedArray = new Float32Array(4); + Atomics.waitAsync(typedArray, 0, 0, 0); + }).toThrowWithMessage( + TypeError, + "Typed array Float32Array element type is not Int32 or BigInt64" + ); + }); + + test("non-shared ArrayBuffer", () => { + expect(() => { + const typedArray = new Int32Array(4); + Atomics.waitAsync(typedArray, 0, 0, 0); + }).toThrowWithMessage( + TypeError, + "The TypedArray's underlying buffer must be a SharedArrayBuffer" + ); + }); + + test("invalid index", () => { + expect(() => { + const buffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT); + const typedArray = new Int32Array(buffer); + + Atomics.waitAsync(typedArray, 4, 0, 0); + }).toThrowWithMessage(RangeError, "Index 4 is out of range of array length 4"); + }); + + test("invalid value", () => { + expect(() => { + const buffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT); + const typedArray = new Int32Array(buffer); + + Atomics.waitAsync(typedArray, 0, Symbol.hasInstance, 0); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + + expect(() => { + const buffer = new SharedArrayBuffer(4 * BigInt64Array.BYTES_PER_ELEMENT); + const typedArray = new BigInt64Array(buffer); + + Atomics.waitAsync(typedArray, 0, Symbol.hasInstance, 0); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to BigInt"); + }); + + test("invalid timeout", () => { + expect(() => { + const buffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT); + const typedArray = new Int32Array(buffer); + + Atomics.waitAsync(typedArray, 0, 0, Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + }); +}); + +test("basic functionality", () => { + test("invariants", () => { + expect(Atomics.waitAsync).toHaveLength(4); + }); +});