diff --git a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs index d9e6468567..7e9106e550 100644 --- a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs +++ b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs @@ -29,6 +29,11 @@ use super::*; +enum TypedArrayProtoReceiver { + TypedArray(*mut crate::typedarray::TypedArrayHeader), + Uint8Buffer(usize), +} + /// The TypedArray prototype methods whose receiver must be brand-checked. The /// `u32` is the spec `.length` (own-property arity), matching what Node reports /// for `Int8Array.prototype..length`. Iterator/data methods that don't take @@ -82,6 +87,13 @@ pub(super) fn install_typed_array_proto_methods(proto_obj: *mut ObjectHeader) { for &(name, arity) in TYPED_ARRAY_PROTO_METHODS { let func_ptr = thunk_for(name); ipm(proto_obj, name, func_ptr, arity); + // `install_proto_method` uses the visible spec `.length` as the call + // arity. These thunks have a wider native signature so optional trailing + // arguments can be padded with `undefined` instead of reading an unset + // register slot. + if matches!(name, "copyWithin" | "fill") { + crate::closure::js_register_closure_arity(func_ptr, 3); + } } } @@ -138,9 +150,9 @@ fn throw_not_typed_array(method: &str) -> ! { } /// Read the `IMPLICIT_THIS` receiver and brand-check it as a real TypedArray. -/// Returns the cleaned `TypedArrayHeader` pointer, or throws a `TypeError`. +/// Returns the cleaned receiver, or throws a `TypeError`. #[inline] -unsafe fn ta_receiver_or_throw(method: &str) -> *mut crate::typedarray::TypedArrayHeader { +unsafe fn ta_receiver_or_throw(method: &str) -> TypedArrayProtoReceiver { let bits = IMPLICIT_THIS.with(|c| c.get()); // A TypedArray receiver reaches here in either of two boxings: a NaN-boxed // `POINTER_TAG` value (top16 >= 0x7FF8) or a *raw* heap pointer whose top16 @@ -156,32 +168,320 @@ unsafe fn ta_receiver_or_throw(method: &str) -> *mut crate::typedarray::TypedArr throw_not_typed_array(method) }; if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - return addr as *mut crate::typedarray::TypedArrayHeader; + return TypedArrayProtoReceiver::TypedArray( + addr as *mut crate::typedarray::TypedArrayHeader, + ); + } + if is_typed_array_buffer(addr) { + return TypedArrayProtoReceiver::Uint8Buffer(addr); } throw_not_typed_array(method) } +fn is_typed_array_buffer(addr: usize) -> bool { + crate::buffer::is_registered_buffer(addr) + && !crate::buffer::is_any_array_buffer(addr) + && !crate::buffer::is_data_view(addr) + && !crate::buffer::is_secret_key(addr) + && crate::buffer::asymmetric_key_meta(addr).is_none() + && crate::buffer::crypto_key_meta(addr).is_none() +} + +#[inline] +fn undefined() -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +#[inline] +fn pointer_value(addr: usize) -> f64 { + f64::from_bits(crate::value::JSValue::pointer(addr as *mut u8).bits()) +} + +#[inline] +fn arg_or_undefined(args: &[f64], index: usize) -> f64 { + args.get(index).copied().unwrap_or_else(undefined) +} + +#[inline] +fn is_undefined_arg(value: f64) -> bool { + crate::value::JSValue::from_bits(value.to_bits()).is_undefined() +} + +#[inline] +fn to_integer_or_infinity(value: f64) -> f64 { + let number = crate::value::JSValue::from_bits(value.to_bits()).to_number(); + if number.is_nan() || number == 0.0 { + 0.0 + } else if !number.is_finite() { + number + } else { + number.trunc() + } +} + +#[inline] +fn uint8_relative_index_arg(args: &[f64], index: usize, len: usize, default: usize) -> usize { + let Some(value) = args.get(index).copied() else { + return default; + }; + if is_undefined_arg(value) { + return default; + } + let n = to_integer_or_infinity(value); + if n == f64::INFINITY { + return len; + } + if n == f64::NEG_INFINITY { + return 0; + } + let n = n as i64; + if n < 0 { + (len as i64 + n).max(0) as usize + } else { + (n as usize).min(len) + } +} + +#[inline] +fn to_uint8(value: f64) -> u8 { + let number = crate::value::JSValue::from_bits(value.to_bits()).to_number(); + if !number.is_finite() || number == 0.0 { + 0 + } else { + (number.trunc() as i64).rem_euclid(256) as u8 + } +} + +#[inline] +unsafe fn uint8_len(addr: usize) -> usize { + (*(addr as *const crate::buffer::BufferHeader)).length as usize +} + +#[inline] +unsafe fn uint8_get(addr: usize, index: usize) -> u8 { + crate::buffer::js_buffer_get(addr as *const crate::buffer::BufferHeader, index as i32) as u8 +} + +#[inline] +unsafe fn uint8_set(addr: usize, index: usize, value: u8) { + crate::buffer::js_buffer_set( + addr as *mut crate::buffer::BufferHeader, + index as i32, + value as i32, + ); +} + +unsafe fn uint8_alloc_like(source_addr: usize, len: usize) -> *mut crate::buffer::BufferHeader { + let out = crate::buffer::buffer_alloc(len as u32); + if !out.is_null() { + (*out).length = len as u32; + if crate::buffer::is_uint8array_buffer(source_addr) { + crate::buffer::mark_as_uint8array(out as usize); + } + } + out +} + +unsafe fn uint8_copy_to_new(source_addr: usize) -> *mut crate::buffer::BufferHeader { + let len = uint8_len(source_addr); + let out = uint8_alloc_like(source_addr, len); + for i in 0..len { + uint8_set(out as usize, i, uint8_get(source_addr, i)); + } + out +} + +fn validate_callback(args: &[f64]) -> *const crate::closure::ClosureHeader { + crate::array::js_validate_array_callback(arg_or_undefined(args, 0)) + as *const crate::closure::ClosureHeader +} + +fn validate_comparator(args: &[f64]) -> *const crate::closure::ClosureHeader { + if args.is_empty() { + std::ptr::null() + } else { + crate::array::js_validate_array_comparator(args[0]) as *const crate::closure::ClosureHeader + } +} + +unsafe fn dispatch_uint8_buffer_method(addr: usize, method: &str, args: &[f64]) -> Option { + let len = uint8_len(addr); + let receiver = pointer_value(addr); + let mut args_ptr = std::ptr::null(); + if !args.is_empty() { + args_ptr = args.as_ptr(); + } + + let result = match method { + "set" => super::dispatch_buffer_method(addr, method, args_ptr, args.len()), + "slice" | "subarray" => { + let start = uint8_relative_index_arg(args, 0, len, 0); + let end = uint8_relative_index_arg(args, 1, len, len); + let result = crate::buffer::js_buffer_slice( + addr as *mut crate::buffer::BufferHeader, + start as i32, + end as i32, + ); + if crate::buffer::is_uint8array_buffer(addr) { + crate::buffer::mark_as_uint8array(result as usize); + } + pointer_value(result as usize) + } + "copyWithin" => { + let to = uint8_relative_index_arg(args, 0, len, 0); + let from = uint8_relative_index_arg(args, 1, len, 0); + let final_ = uint8_relative_index_arg(args, 2, len, len); + let count = final_.saturating_sub(from).min(len.saturating_sub(to)); + if count > 0 { + let block: Vec = (0..count).map(|i| uint8_get(addr, from + i)).collect(); + for (i, value) in block.into_iter().enumerate() { + uint8_set(addr, to + i, value); + } + } + receiver + } + "fill" => { + let value = to_uint8(arg_or_undefined(args, 0)); + let start = uint8_relative_index_arg(args, 1, len, 0); + let end = uint8_relative_index_arg(args, 2, len, len); + for i in start..end { + uint8_set(addr, i, value); + } + receiver + } + "map" => { + let cb = validate_callback(args); + let out = uint8_alloc_like(addr, len); + for i in 0..len { + let value = uint8_get(addr, i) as f64; + let mapped = crate::closure::js_closure_call3(cb, value, i as f64, receiver); + uint8_set(out as usize, i, to_uint8(mapped)); + } + pointer_value(out as usize) + } + "filter" => { + let cb = validate_callback(args); + let mut kept = Vec::new(); + for i in 0..len { + let value = uint8_get(addr, i) as f64; + let keep = crate::closure::js_closure_call3(cb, value, i as f64, receiver); + if crate::value::js_is_truthy(keep) != 0 { + kept.push(value as u8); + } + } + let out = uint8_alloc_like(addr, kept.len()); + for (i, value) in kept.into_iter().enumerate() { + uint8_set(out as usize, i, value); + } + pointer_value(out as usize) + } + "findIndex" | "findLastIndex" => { + let cb = validate_callback(args); + let indexes: Box> = if method == "findIndex" { + Box::new(0..len) + } else { + Box::new((0..len).rev()) + }; + for i in indexes { + let value = uint8_get(addr, i) as f64; + let keep = crate::closure::js_closure_call3(cb, value, i as f64, receiver); + if crate::value::js_is_truthy(keep) != 0 { + return Some(i as f64); + } + } + -1.0 + } + "reverse" => { + if len > 1 { + let mut i = 0usize; + let mut j = len - 1; + while i < j { + let left = uint8_get(addr, i); + let right = uint8_get(addr, j); + uint8_set(addr, i, right); + uint8_set(addr, j, left); + i += 1; + j -= 1; + } + } + receiver + } + "sort" | "toSorted" => { + let cmp = validate_comparator(args); + let out_addr = if method == "sort" { + addr + } else { + uint8_copy_to_new(addr) as usize + }; + let mut values: Vec = (0..len).map(|i| uint8_get(out_addr, i)).collect(); + if cmp.is_null() { + values.sort_unstable(); + } else { + values.sort_by(|a, b| { + let r = crate::closure::js_closure_call2(cmp, *a as f64, *b as f64); + if r < 0.0 { + std::cmp::Ordering::Less + } else if r > 0.0 { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + }); + } + for (i, value) in values.into_iter().enumerate() { + uint8_set(out_addr, i, value); + } + if method == "sort" { + receiver + } else { + pointer_value(out_addr) + } + } + "toReversed" => { + let out = uint8_alloc_like(addr, len); + for i in 0..len { + uint8_set(out as usize, i, uint8_get(addr, len - 1 - i)); + } + pointer_value(out as usize) + } + _ => return None, + }; + Some(result) +} + /// Brand-check, then delegate to the shared dispatch tower with the supplied /// argument slice. `dispatch_typed_array_method` handles every method name in /// `TYPED_ARRAY_PROTO_METHODS`; the `unwrap_or(undefined)` guard never fires in /// practice (the name set is kept in sync) but avoids a panic on drift. #[inline] unsafe fn brand_then_dispatch(method: &str, args: &[f64]) -> f64 { - let ta = ta_receiver_or_throw(method); + let receiver = ta_receiver_or_throw(method); let args_ptr = if args.is_empty() { std::ptr::null() } else { args.as_ptr() }; - match super::native_call_method::dispatch_typed_array_method(ta, method, args_ptr, args.len()) { - // Brand check passed and the tower handled the method. - Some(r) => r, - // The only `TYPED_ARRAY_PROTO_METHODS` entry the tower doesn't yet - // resolve is `toLocaleString` (a separate formatting gap, out of scope - // for this brand-check fix). The brand check already ran, so a - // non-TypedArray receiver has thrown; a real receiver simply gets - // `undefined` here rather than a wrong value. - None => f64::from_bits(crate::value::TAG_UNDEFINED), + match receiver { + TypedArrayProtoReceiver::TypedArray(ta) => { + match super::native_call_method::dispatch_typed_array_method( + ta, + method, + args_ptr, + args.len(), + ) { + // Brand check passed and the tower handled the method. + Some(r) => r, + // The only `TYPED_ARRAY_PROTO_METHODS` entry the tower doesn't yet + // resolve is `toLocaleString` (a separate formatting gap, out of scope + // for this brand-check fix). The brand check already ran, so a + // non-TypedArray receiver has thrown; a real receiver simply gets + // `undefined` here rather than a wrong value. + None => undefined(), + } + } + TypedArrayProtoReceiver::Uint8Buffer(addr) => { + dispatch_uint8_buffer_method(addr, method, args).unwrap_or_else(undefined) + } } } diff --git a/test-parity/node-suite/globals/typedarray-branded-methods.ts b/test-parity/node-suite/globals/typedarray-branded-methods.ts new file mode 100644 index 0000000000..f49b3e1e0d --- /dev/null +++ b/test-parity/node-suite/globals/typedarray-branded-methods.ts @@ -0,0 +1,128 @@ +// @ts-nocheck + +function showError(label, fn) { + try { + fn(); + console.log(label + ":NO_THROW"); + } catch (error) { + console.log(label + ":" + error.constructor.name); + } +} + +function showView(label, value) { + console.log( + label + ":" + value.constructor.name + ":" + Array.from(value).join(","), + ); +} + +function showMutating(label, method, receiver, ...args) { + const returned = Int16Array.prototype[method].call(receiver, ...args); + console.log( + label + + ":" + + (returned === receiver) + + ":" + + receiver.constructor.name + + ":" + + Array.from(receiver).join(","), + ); +} + +const nonTypedArrayCases = [ + ["map", [function (value) { return value; }]], + ["filter", [function () { return true; }]], + ["slice", [0, 1]], + ["subarray", [0, 1]], + ["copyWithin", [0, 1]], + ["fill", [7]], + ["reverse", []], + ["sort", []], + ["toReversed", []], + ["toSorted", []], + ["findIndex", [function () { return true; }]], + ["findLastIndex", [function () { return true; }]], + ["set", [[1], 0]], +]; + +for (const [method, args] of nonTypedArrayCases) { + showError(method + " array receiver", function () { + Int16Array.prototype[method].call([1, 2, 3], ...args); + }); + showError(method + " inherited receiver", function () { + Int16Array.prototype[method].call(Object.create(Uint8Array.prototype), ...args); + }); +} + +showView( + "map borrowed", + Int16Array.prototype.map.call(new Uint8Array([3, 4]), function (value) { + return value + 1; + }), +); +showView( + "filter borrowed", + Int16Array.prototype.filter.call(new Uint8Array([1, 2, 3]), function (value) { + return value > 1; + }), +); +showView("slice borrowed", Int16Array.prototype.slice.call(new Uint8Array([5, 6, 7]), 1)); +showView( + "subarray borrowed", + Int16Array.prototype.subarray.call(new Uint8Array([8, 9, 10]), 1), +); +showView( + "toReversed borrowed", + Int16Array.prototype.toReversed.call(new Uint8Array([1, 2, 3])), +); +showView( + "toSorted borrowed", + Int16Array.prototype.toSorted.call(new Uint8Array([3, 1, 2])), +); + +showMutating("copyWithin borrowed", "copyWithin", new Uint8Array([1, 2, 3]), 0, 1); +showMutating("fill borrowed", "fill", new Uint8Array([1, 2, 3]), 9, 1); +showMutating("reverse borrowed", "reverse", new Uint8Array([1, 2, 3])); +showMutating("sort borrowed", "sort", new Uint8Array([3, 1, 2])); + +showView( + "buffer map borrowed", + Int16Array.prototype.map.call(Buffer.from([3, 4]), function (value) { + return value + 1; + }), +); +showView("buffer slice borrowed", Int16Array.prototype.slice.call(Buffer.from([5, 6, 7]), 1)); + +const bufferFillReceiver = Buffer.from([1, 2, 3]); +const bufferFillReturned = Int16Array.prototype.fill.call(bufferFillReceiver, 9, 1); +console.log( + "buffer fill borrowed:" + + (bufferFillReturned === bufferFillReceiver) + + ":" + + bufferFillReceiver.constructor.name + + ":" + + Array.from(bufferFillReceiver).join(","), +); + +const setReceiver = new Uint8Array([0, 0, 0]); +const setReturned = Int16Array.prototype.set.call(setReceiver, [4, 5], 1); +console.log( + "set borrowed:" + + String(setReturned) + + ":" + + setReceiver.constructor.name + + ":" + + Array.from(setReceiver).join(","), +); + +console.log( + "findIndex borrowed:" + + Int16Array.prototype.findIndex.call(new Uint8Array([1, 2, 3]), function (value) { + return value > 1; + }), +); +console.log( + "findLastIndex borrowed:" + + Int16Array.prototype.findLastIndex.call(new Uint8Array([1, 2, 3]), function (value) { + return value < 3; + }), +);