diff --git a/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp b/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp index b58196ef25..62d0d8de26 100644 --- a/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp @@ -48,6 +48,7 @@ void TypedArrayPrototype::initialize(GlobalObject& object) define_native_function(vm.names.reverse, reverse, 0, attr); define_native_function(vm.names.copyWithin, copy_within, 2, attr); define_native_function(vm.names.filter, filter, 1, attr); + define_native_function(vm.names.map, map, 1, attr); define_native_accessor(*vm.well_known_symbol_to_string_tag(), to_string_tag_getter, nullptr, Attribute::Configurable); @@ -1096,4 +1097,54 @@ JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::filter) return filter_array; } +// 23.2.3.19 %TypedArray%.prototype.map ( callbackfn [ , thisArg ] ), https://tc39.es/ecma262/#sec-%typedarray%.prototype.map +JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::map) +{ + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + auto* typed_array = typed_array_from(vm, global_object); + if (!typed_array) + return {}; + + // 3. Let len be O.[[ArrayLength]]. + auto initial_length = typed_array->array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + auto* callback_function = callback_from_args(global_object, "map"); + if (!callback_function) + return {}; + + // 5. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(len) »). + MarkedValueList arguments(vm.heap()); + arguments.empend(initial_length); + auto* return_array = typed_array_species_create(global_object, *typed_array, move(arguments)); + if (vm.exception()) + return {}; + + auto this_value = vm.argument(1); + + // 6. Let k be 0. + // 7. Repeat, while k < len, + for (size_t i = 0; i < initial_length; ++i) { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + auto value = typed_array->get(i); + + // c. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). + auto mapped_value = vm.call(*callback_function, this_value, value, Value((i32)i), typed_array); + if (vm.exception()) + return {}; + + // d. Perform ? Set(A, Pk, mappedValue, true). + return_array->set(i, mapped_value, true); + if (vm.exception()) + return {}; + + // e. Set k to k + 1. + } + + // 8. Return A. + return return_array; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.h b/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.h index 71f2f9ad81..245239dd35 100644 --- a/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/TypedArrayPrototype.h @@ -45,6 +45,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(reverse); JS_DECLARE_NATIVE_FUNCTION(copy_within); JS_DECLARE_NATIVE_FUNCTION(filter); + JS_DECLARE_NATIVE_FUNCTION(map); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.prototype.map.js b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.prototype.map.js new file mode 100644 index 0000000000..048a3eb06c --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.prototype.map.js @@ -0,0 +1,259 @@ +const TYPED_ARRAYS = [ + Uint8Array, + Uint8ClampedArray, + Uint16Array, + Uint32Array, + Int8Array, + Int16Array, + Int32Array, + Float32Array, + Float64Array, +]; + +const BIGINT_TYPED_ARRAYS = [BigUint64Array, BigInt64Array]; + +test("length is 1", () => { + TYPED_ARRAYS.forEach(T => { + expect(T.prototype.map).toHaveLength(1); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + expect(T.prototype.map).toHaveLength(1); + }); +}); + +describe("errors", () => { + function argumentErrorTests(T) { + test(`requires at least one argument (${T.name})`, () => { + expect(() => { + new T().map(); + }).toThrowWithMessage( + TypeError, + "TypedArray.prototype.map() requires at least one argument" + ); + }); + + test(`callback must be a function (${T.name})`, () => { + expect(() => { + new T().map(undefined); + }).toThrowWithMessage(TypeError, "undefined is not a function"); + }); + } + + TYPED_ARRAYS.forEach(T => argumentErrorTests(T)); + BIGINT_TYPED_ARRAYS.forEach(T => argumentErrorTests(T)); + + test("throws if mappedValue is not the same type of the typed array", () => { + TYPED_ARRAYS.forEach(T => { + let callbackCalled = 0; + let result; + + expect(() => { + result = new T([1, 2, 3]).map((value, index) => { + callbackCalled++; + return index % 2 === 0 ? 1n : 1; + }); + }).toThrowWithMessage(TypeError, "Cannot convert BigInt to number"); + + expect(callbackCalled).toBe(1); + expect(result).toBeUndefined(); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + let callbackCalled = 0; + let result; + + expect(() => { + result = new T([1n, 2n, 3n]).map((value, index) => { + callbackCalled++; + return index % 2 === 0 ? 1 : 1n; + }); + }).toThrowWithMessage(TypeError, "Cannot convert number to BigInt"); + + expect(callbackCalled).toBe(1); + expect(result).toBeUndefined(); + }); + }); + + test("Symbol.species returns a typed array with a different content type", () => { + TYPED_ARRAYS.forEach(T => { + class TypedArray extends T { + static get [Symbol.species]() { + return BigUint64Array; + } + } + + let result; + + expect(() => { + result = new TypedArray().map(() => {}); + }).toThrowWithMessage(TypeError, `Can't create BigUint64Array from ${T.name}`); + + expect(result).toBeUndefined(); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + class TypedArray extends T { + static get [Symbol.species]() { + return Uint32Array; + } + } + + let result; + + expect(() => { + result = new TypedArray().map(() => {}); + }).toThrowWithMessage(TypeError, `Can't create Uint32Array from ${T.name}`); + + expect(result).toBeUndefined(); + }); + }); + + test("Symbol.species doesn't return a typed array", () => { + TYPED_ARRAYS.forEach(T => { + class TypedArray extends T { + static get [Symbol.species]() { + return Array; + } + } + + let result; + + expect(() => { + result = new TypedArray().map(() => {}); + }).toThrowWithMessage(TypeError, "Not a TypedArray object"); + + expect(result).toBeUndefined(); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + class TypedArray extends T { + static get [Symbol.species]() { + return Array; + } + } + + let result; + + expect(() => { + result = new TypedArray().map(() => {}); + }).toThrowWithMessage(TypeError, "Not a TypedArray object"); + + expect(result).toBeUndefined(); + }); + }); +}); + +describe("normal behaviour", () => { + test("never calls callback with empty array", () => { + TYPED_ARRAYS.forEach(T => { + let callbackCalled = 0; + expect( + new T([]).map(() => { + callbackCalled++; + }) + ).toHaveLength(0); + expect(callbackCalled).toBe(0); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + let callbackCalled = 0; + expect( + new T([]).map(() => { + callbackCalled++; + }) + ).toHaveLength(0); + expect(callbackCalled).toBe(0); + }); + }); + + test("calls callback once for every item", () => { + TYPED_ARRAYS.forEach(T => { + let callbackCalled = 0; + expect( + new T([1, 2, 3]).map(value => { + callbackCalled++; + // NOTE: This is just to prevent a conversion exception. + return value; + }) + ).toHaveLength(3); + expect(callbackCalled).toBe(3); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + let callbackCalled = 0; + expect( + new T([1n, 2n, 3n]).map(value => { + callbackCalled++; + // NOTE: This is just to prevent a conversion exception. + return value; + }) + ).toHaveLength(3); + expect(callbackCalled).toBe(3); + }); + }); + + test("can map based on callback return value", () => { + TYPED_ARRAYS.forEach(T => { + const squaredNumbers = new T([0, 1, 2, 3, 4]).map(x => x ** 2); + expect(squaredNumbers).toHaveLength(5); + expect(squaredNumbers[0]).toBe(0); + expect(squaredNumbers[1]).toBe(1); + expect(squaredNumbers[2]).toBe(4); + expect(squaredNumbers[3]).toBe(9); + expect(squaredNumbers[4]).toBe(16); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + const squaredNumbers = new T([0n, 1n, 2n, 3n, 4n]).map(x => x ** 2n); + expect(squaredNumbers).toHaveLength(5); + expect(squaredNumbers[0]).toBe(0n); + expect(squaredNumbers[1]).toBe(1n); + expect(squaredNumbers[2]).toBe(4n); + expect(squaredNumbers[3]).toBe(9n); + expect(squaredNumbers[4]).toBe(16n); + }); + }); + + test("Symbol.species returns a typed array with a matching content type", () => { + TYPED_ARRAYS.forEach(T => { + class TypedArray extends T { + static get [Symbol.species]() { + return Uint32Array; + } + } + + let result; + + expect(() => { + result = new TypedArray([1, 2, 3]).map(value => value + 2); + }).not.toThrowWithMessage(TypeError, `Can't create Uint32Array from ${T.name}`); + + expect(result).toBeInstanceOf(Uint32Array); + expect(result).toHaveLength(3); + expect(result[0]).toBe(3); + expect(result[1]).toBe(4); + expect(result[2]).toBe(5); + }); + + BIGINT_TYPED_ARRAYS.forEach(T => { + class TypedArray extends T { + static get [Symbol.species]() { + return BigUint64Array; + } + } + + let result; + + expect(() => { + result = new TypedArray([1n, 2n, 3n]).map(value => value + 2n); + }).not.toThrowWithMessage(TypeError, `Can't create BigUint64Array from ${T.name}`); + + expect(result).toBeInstanceOf(BigUint64Array); + expect(result).toHaveLength(3); + expect(result[0]).toBe(3n); + expect(result[1]).toBe(4n); + expect(result[2]).toBe(5n); + }); + }); +});