diff --git a/boa_engine/src/builtins/array/mod.rs b/boa_engine/src/builtins/array/mod.rs index 05b7abc775..ba1efc4a0f 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -82,6 +82,10 @@ impl IntrinsicObject for Array { let unscopables_object = Self::unscopables_object(); BuiltInBuilder::from_standard_constructor::(realm) + // Static Methods + .static_method(Self::from, js_string!("from"), 1) + .static_method(Self::is_array, js_string!("isArray"), 1) + .static_method(Self::of, js_string!("of"), 0) .static_accessor( JsSymbol::species(), Some(get_species), @@ -93,62 +97,62 @@ impl IntrinsicObject for Array { 0, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, ) - .property( - utf16!("values"), - values_function.clone(), - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .property( - symbol_iterator, - values_function, - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .property( - symbol_unscopables, - unscopables_object, - Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) .method(Self::at, js_string!("at"), 1) .method(Self::concat, js_string!("concat"), 1) - .method(Self::push, js_string!("push"), 1) - .method(Self::index_of, js_string!("indexOf"), 1) - .method(Self::last_index_of, js_string!("lastIndexOf"), 1) - .method(Self::includes_value, js_string!("includes"), 1) - .method(Self::map, js_string!("map"), 1) + .method(Self::copy_within, js_string!("copyWithin"), 2) + .method(Self::entries, js_string!("entries"), 0) + .method(Self::every, js_string!("every"), 1) .method(Self::fill, js_string!("fill"), 1) - .method(Self::for_each, js_string!("forEach"), 1) .method(Self::filter, js_string!("filter"), 1) - .method(Self::pop, js_string!("pop"), 0) - .method(Self::join, js_string!("join"), 1) - .property( - utf16!("toString"), - to_string_function, - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .method(Self::reverse, js_string!("reverse"), 0) - .method(Self::shift, js_string!("shift"), 0) - .method(Self::unshift, js_string!("unshift"), 1) - .method(Self::every, js_string!("every"), 1) .method(Self::find, js_string!("find"), 1) .method(Self::find_index, js_string!("findIndex"), 1) .method(Self::find_last, js_string!("findLast"), 1) .method(Self::find_last_index, js_string!("findLastIndex"), 1) .method(Self::flat, js_string!("flat"), 0) .method(Self::flat_map, js_string!("flatMap"), 1) + .method(Self::for_each, js_string!("forEach"), 1) + .method(Self::includes_value, js_string!("includes"), 1) + .method(Self::index_of, js_string!("indexOf"), 1) + .method(Self::join, js_string!("join"), 1) + .method(Self::keys, js_string!("keys"), 0) + .method(Self::last_index_of, js_string!("lastIndexOf"), 1) + .method(Self::map, js_string!("map"), 1) + .method(Self::pop, js_string!("pop"), 0) + .method(Self::push, js_string!("push"), 1) + .method(Self::reduce, js_string!("reduce"), 1) + .method(Self::reduce_right, js_string!("reduceRight"), 1) + .method(Self::reverse, js_string!("reverse"), 0) + .method(Self::shift, js_string!("shift"), 0) .method(Self::slice, js_string!("slice"), 2) .method(Self::some, js_string!("some"), 1) .method(Self::sort, js_string!("sort"), 1) .method(Self::splice, js_string!("splice"), 2) .method(Self::to_locale_string, js_string!("toLocaleString"), 0) - .method(Self::reduce, js_string!("reduce"), 1) - .method(Self::reduce_right, js_string!("reduceRight"), 1) - .method(Self::keys, js_string!("keys"), 0) - .method(Self::entries, js_string!("entries"), 0) - .method(Self::copy_within, js_string!("copyWithin"), 2) - // Static Methods - .static_method(Self::from, js_string!("from"), 1) - .static_method(Self::is_array, js_string!("isArray"), 1) - .static_method(Self::of, js_string!("of"), 0) + .method(Self::to_reversed, js_string!("toReversed"), 0) + .method(Self::to_sorted, js_string!("toSorted"), 1) + .method(Self::to_spliced, js_string!("toSpliced"), 2) + .method(Self::unshift, js_string!("unshift"), 1) + .method(Self::with, js_string!("with"), 2) + .property( + utf16!("toString"), + to_string_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + utf16!("values"), + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + symbol_iterator, + values_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + symbol_unscopables, + unscopables_object, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) .build(); } @@ -1130,6 +1134,46 @@ impl Array { Ok(o.into()) } + /// [`Array.prototype.toReversed()`][spec] + /// + /// Reverses this array, returning the result into a copy of the array. + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.toreversed + pub(crate) fn to_reversed( + this: &JsValue, + _: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. Let A be ? ArrayCreate(len). + let a = Array::array_create(len, None, context)?; + + // 4. Let k be 0. + // 5. Repeat, while k < len, + for i in 0..len { + // a. Let from be ! ToString(๐”ฝ(len - k - 1)). + let from = len - i - 1; + + // b. Let Pk be ! ToString(๐”ฝ(k)). + // c. Let fromValue be ? Get(O, from). + let from_value = o.get(from, context)?; + + // d. Perform ! CreateDataPropertyOrThrow(A, Pk, fromValue). + a.create_data_property_or_throw(i, from_value, context) + .expect("cannot fail per the spec"); + + // e. Set k to k + 1. + } + + // 6. Return A. + Ok(a.into()) + } + /// `Array.prototype.shift()` /// /// The first element of the array is removed from the array and returned. @@ -1938,13 +1982,13 @@ impl Array { // 4. If relativeStart is -โˆž, let k be 0. // 5. Else if relativeStart < 0, let k be max(len + relativeStart, 0). // 6. Else, let k be min(relativeStart, len). - let mut k = Self::get_relative_start(context, args.get(1), len)?; + let mut k = Self::get_relative_start(context, args.get_or_undefined(1), len)?; // 7. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). // 8. If relativeEnd is -โˆž, let final be 0. // 9. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). // 10. Else, let final be min(relativeEnd, len). - let final_ = Self::get_relative_end(context, args.get(2), len)?; + let final_ = Self::get_relative_end(context, args.get_or_undefined(2), len)?; let value = args.get_or_undefined(0); @@ -2063,13 +2107,13 @@ impl Array { // 4. If relativeStart is -โˆž, let k be 0. // 5. Else if relativeStart < 0, let k be max(len + relativeStart, 0). // 6. Else, let k be min(relativeStart, len). - let mut k = Self::get_relative_start(context, args.get(0), len)?; + let mut k = Self::get_relative_start(context, args.get_or_undefined(0), len)?; // 7. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). // 8. If relativeEnd is -โˆž, let final be 0. // 9. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). // 10. Else, let final be min(relativeEnd, len). - let final_ = Self::get_relative_end(context, args.get(1), len)?; + let final_ = Self::get_relative_end(context, args.get_or_undefined(1), len)?; // 11. Let count be max(final - k, 0). let count = final_.saturating_sub(k); @@ -2171,6 +2215,43 @@ impl Array { Ok(js_string!(r).into()) } + /// Gets the delete count of a splice operation. + fn get_delete_count( + len: u64, + actual_start: u64, + start: Option<&JsValue>, + delete_count: Option<&JsValue>, + context: &mut Context<'_>, + ) -> JsResult { + // 8. If start is not present, then + let actual_delete_count = if start.is_none() { + // a. Let actualDeleteCount be 0. + 0 + } + // 10. Else, + else if let Some(delete_count) = delete_count { + // a. Let dc be ? ToIntegerOrInfinity(deleteCount). + let dc = delete_count.to_integer_or_infinity(context)?; + + // b. Let actualDeleteCount be the result of clamping dc between 0 and len - actualStart. + let max = len - actual_start; + match dc { + IntegerOrInfinity::Integer(i) => u64::try_from(i) + .unwrap_or_default() + .clamp(0, len - actual_start), + IntegerOrInfinity::PositiveInfinity => max, + IntegerOrInfinity::NegativeInfinity => 0, + } + } + // 9. Else if deleteCount is not present, then + else { + // a. Let actualDeleteCount be len - actualStart. + len - actual_start + }; + + Ok(actual_delete_count) + } + /// `Array.prototype.splice ( start, [deleteCount[, ...items]] )` /// /// Splices an array by following @@ -2188,109 +2269,85 @@ impl Array { let start = args.get(0); let delete_count = args.get(1); - let items = args.get(2..).unwrap_or(&[]); + let items = args.get(2..).unwrap_or_default(); + // 3. Let relativeStart be ? ToIntegerOrInfinity(start). - // 4. If relativeStart is -โˆž, let actualStart be 0. + // 4. If relativeStart = -โˆž, let actualStart be 0. // 5. Else if relativeStart < 0, let actualStart be max(len + relativeStart, 0). // 6. Else, let actualStart be min(relativeStart, len). - let actual_start = Self::get_relative_start(context, start, len)?; - // 7. If start is not present, then - let insert_count = if start.is_none() || delete_count.is_none() { - // 7a. Let insertCount be 0. - // 8. Else if deleteCount is not present, then - // a. Let insertCount be 0. - 0 - // 9. Else, - } else { - // 9a. Let insertCount be the number of elements in items. - items.len() as u64 - }; - let actual_delete_count = if start.is_none() { - // 7b. Let actualDeleteCount be 0. - 0 - // 8. Else if deleteCount is not present, then - } else if delete_count.is_none() { - // 8b. Let actualDeleteCount be len - actualStart. - len - actual_start - // 9. Else, - } else { - // b. Let dc be ? ToIntegerOrInfinity(deleteCount). - let dc = delete_count - .cloned() - .unwrap_or_default() - .to_integer_or_infinity(context)?; - // c. Let actualDeleteCount be the result of clamping dc between 0 and len - actualStart. - let max = len - actual_start; - match dc { - IntegerOrInfinity::Integer(i) => u64::try_from(i).unwrap_or_default().clamp(0, max), - IntegerOrInfinity::PositiveInfinity => max, - IntegerOrInfinity::NegativeInfinity => 0, - } - }; + let actual_start = + Self::get_relative_start(context, start.unwrap_or(&JsValue::undefined()), len)?; + + // 7. Let itemCount be the number of elements in items. + let item_count = items.len() as u64; - // 10. If len + insertCount - actualDeleteCount > 2^53 - 1, throw a TypeError exception. - if len + insert_count - actual_delete_count > Number::MAX_SAFE_INTEGER as u64 { + let actual_delete_count = + Self::get_delete_count(len, actual_start, start, delete_count, context)?; + + // If len + itemCount - actualDeleteCount > 2**53 - 1, throw a TypeError exception. + if len + item_count - actual_delete_count > Number::MAX_SAFE_INTEGER as u64 { return Err(JsNativeError::typ() .with_message("Target splice exceeded max safe integer value") .into()); } - // 11. Let A be ? ArraySpeciesCreate(O, actualDeleteCount). + // 12. Let A be ? ArraySpeciesCreate(O, actualDeleteCount). let arr = Self::array_species_create(&o, actual_delete_count, context)?; - // 12. Let k be 0. - // 13. Repeat, while k < actualDeleteCount, + + // 13. Let k be 0. + // 14. Repeat, while k < actualDeleteCount, for k in 0..actual_delete_count { // a. Let from be ! ToString(๐”ฝ(actualStart + k)). - // b. Let fromPresent be ? HasProperty(O, from). - let from_present = o.has_property(actual_start + k, context)?; - // c. If fromPresent is true, then - if from_present { + // b. If ? HasProperty(O, from) is true, then + if o.has_property(actual_start + k, context)? { // i. Let fromValue be ? Get(O, from). let from_value = o.get(actual_start + k, context)?; + // ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(๐”ฝ(k)), fromValue). arr.create_data_property_or_throw(k, from_value, context)?; } - // d. Set k to k + 1. + // c. Set k to k + 1. } - // 14. Perform ? Set(A, "length", ๐”ฝ(actualDeleteCount), true). + // 15. Perform ? Set(A, "length", ๐”ฝ(actualDeleteCount), true). Self::set_length(&arr, actual_delete_count, context)?; - // 15. Let itemCount be the number of elements in items. let item_count = items.len() as u64; match item_count.cmp(&actual_delete_count) { + Ordering::Equal => {} // 16. If itemCount < actualDeleteCount, then Ordering::Less => { - // a. Set k to actualStart. - // b. Repeat, while k < (len - actualDeleteCount), + // a. Set k to actualStart. + // b. Repeat, while k < (len - actualDeleteCount), for k in actual_start..(len - actual_delete_count) { // i. Let from be ! ToString(๐”ฝ(k + actualDeleteCount)). let from = k + actual_delete_count; + // ii. Let to be ! ToString(๐”ฝ(k + itemCount)). let to = k + item_count; - // iii. Let fromPresent be ? HasProperty(O, from). - let from_present = o.has_property(from, context)?; - // iv. If fromPresent is true, then - if from_present { + + // iii. If ? HasProperty(O, from) is true, then + if o.has_property(from, context)? { // 1. Let fromValue be ? Get(O, from). let from_value = o.get(from, context)?; + // 2. Perform ? Set(O, to, fromValue, true). o.set(to, from_value, true, context)?; - // v. Else, } else { - // 1. Assert: fromPresent is false. - debug_assert!(!from_present); - // 2. Perform ? DeletePropertyOrThrow(O, to). + // iv. Else, + // 1. Perform ? DeletePropertyOrThrow(O, to). o.delete_property_or_throw(to, context)?; } - // vi. Set k to k + 1. + // v. Set k to k + 1. } + // c. Set k to len. // d. Repeat, while k > (len - actualDeleteCount + itemCount), for k in ((len - actual_delete_count + item_count)..len).rev() { // i. Perform ? DeletePropertyOrThrow(O, ! ToString(๐”ฝ(k - 1))). o.delete_property_or_throw(k, context)?; + // ii. Set k to k - 1. } } @@ -2301,41 +2358,34 @@ impl Array { for k in (actual_start..len - actual_delete_count).rev() { // i. Let from be ! ToString(๐”ฝ(k + actualDeleteCount - 1)). let from = k + actual_delete_count; + // ii. Let to be ! ToString(๐”ฝ(k + itemCount - 1)). let to = k + item_count; - // iii. Let fromPresent be ? HasProperty(O, from). - let from_present = o.has_property(from, context)?; - // iv. If fromPresent is true, then - if from_present { + + // iii. If ? HasProperty(O, from) is true, then + if o.has_property(from, context)? { // 1. Let fromValue be ? Get(O, from). let from_value = o.get(from, context)?; + // 2. Perform ? Set(O, to, fromValue, true). o.set(to, from_value, true, context)?; - // v. Else, - } else { - // 1. Assert: fromPresent is false. - debug_assert!(!from_present); - // 2. Perform ? DeletePropertyOrThrow(O, to). + } + // iv. Else, + else { + // 1. Perform ? DeletePropertyOrThrow(O, to). o.delete_property_or_throw(to, context)?; } - // vi. Set k to k - 1. + // v. Set k to k - 1. } } - Ordering::Equal => {} - }; + } // 18. Set k to actualStart. // 19. For each element E of items, do - if item_count > 0 { - for (k, item) in items - .iter() - .enumerate() - .map(|(i, val)| (i as u64 + actual_start, val)) - { - // a. Perform ? Set(O, ! ToString(๐”ฝ(k)), E, true). - o.set(k, item.clone(), true, context)?; - // b. Set k to k + 1. - } + for (i, item) in items.iter().enumerate() { + // a. Perform ? Set(O, ! ToString(๐”ฝ(k)), E, true). + // b. Set k to k + 1. + o.set(actual_start + i as u64, item.clone(), true, context)?; } // 20. Perform ? Set(O, "length", ๐”ฝ(len - actualDeleteCount + itemCount), true). @@ -2345,6 +2395,103 @@ impl Array { Ok(JsValue::from(arr)) } + /// [`Array.prototype.toSpliced ( start, skipCount, ...items )`][spec] + /// + /// Splices the target array, returning the result as a new array. + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.tospliced + fn to_spliced( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + let start = args.get(0); + let skip_count = args.get(1); + let items = args.get(2..).unwrap_or_default(); + + // 3. Let relativeStart be ? ToIntegerOrInfinity(start). + // 4. If relativeStart is -โˆž, let actualStart be 0. + // 5. Else if relativeStart < 0, let actualStart be max(len + relativeStart, 0). + // 6. Else, let actualStart be min(relativeStart, len). + let actual_start = + Self::get_relative_start(context, start.unwrap_or(&JsValue::undefined()), len)?; + + // 7. Let insertCount be the number of elements in items. + let insert_count = items.len() as u64; + + let actual_skip_count = + Self::get_delete_count(len, actual_start, start, skip_count, context)?; + + // 11. Let newLen be len + insertCount - actualSkipCount. + let new_len = len + insert_count - actual_skip_count; + + // 12. If newLen > 2**53 - 1, throw a TypeError exception. + if new_len > Number::MAX_SAFE_INTEGER as u64 { + return Err(JsNativeError::typ() + .with_message("Target splice exceeded max safe integer value") + .into()); + } + + // 13. Let A be ? ArrayCreate(newLen). + let arr = Array::array_create(new_len, None, context)?; + + // 14. Let i be 0. + let mut i = 0; + // 16. Repeat, while i < actualStart, + while i < actual_start { + // a. Let Pi be ! ToString(๐”ฝ(i)). + // b. Let iValue be ? Get(O, Pi). + let value = o.get(i, context)?; + + // c. Perform ! CreateDataPropertyOrThrow(A, Pi, iValue). + arr.create_data_property_or_throw(i, value, context) + .expect("cannot fail for a newly created array"); + + // d. Set i to i + 1. + i += 1; + } + + // 17. For each element E of items, do + for item in items.iter().cloned() { + // a. Let Pi be ! ToString(๐”ฝ(i)). + // b. Perform ! CreateDataPropertyOrThrow(A, Pi, E). + arr.create_data_property_or_throw(i, item, context) + .expect("cannot fail for a newly created array"); + + // c. Set i to i + 1. + i += 1; + } + + // 15. Let r be actualStart + actualSkipCount. + let mut r = actual_start + actual_skip_count; + + // 18. Repeat, while i < newLen, + while i < new_len { + // a. Let Pi be ! ToString(๐”ฝ(i)). + // b. Let from be ! ToString(๐”ฝ(r)). + // c. Let fromValue be ? Get(O, from). + let from_value = o.get(r, context)?; + + // d. Perform ! CreateDataPropertyOrThrow(A, Pi, fromValue). + arr.create_data_property_or_throw(i, from_value, context) + .expect("cannot fail for a newly created array"); + + // e. Set i to i + 1. + i += 1; + // f. Set r to r + 1. + r += 1; + } + + // 19. Return A. + Ok(arr.into()) + } + /// `Array.prototype.filter( callback, [ thisArg ] )` /// /// For each element in the array the callback function is called, and a new @@ -2462,6 +2609,65 @@ impl Array { Ok(JsValue::new(false)) } + /// [`SortIndexedProperties ( obj, len, SortCompare, holes )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-sortindexedproperties + pub(crate) fn sort_indexed_properties( + obj: &JsObject, + len: u64, + sort_compare: F, + skip_holes: bool, + context: &mut Context<'_>, + ) -> JsResult> + where + F: Fn(&JsValue, &JsValue, &mut Context<'_>) -> JsResult, + { + // 1. Let items be a new empty List. + // doesn't matter if it clamps since it's just a best-effort optimization + let mut items = Vec::with_capacity(len as usize); + + // 2. Let k be 0. + // 3. Repeat, while k < len, + for i in 0..len { + // a. Let Pk be ! ToString(๐”ฝ(k)). + // b. If holes is skip-holes, then + let read = if skip_holes { + // i. Let kRead be ? HasProperty(obj, Pk). + obj.has_property(i, context)? + } + // c. Else, + else { + // i. Assert: holes is read-through-holes. + // ii. Let kRead be true. + true + }; + + // d. If kRead is true, then + if read { + // i. Let kValue be ? Get(obj, Pk). + // ii. Append kValue to items. + items.push(obj.get(i, context)?); + } + // e. Set k to k + 1. + } + // 4. Sort items using an implementation-defined sequence of calls to SortCompare. If any such call returns an abrupt completion, stop before performing any further calls to SortCompare and return that Completion Record. + let mut sort_err = Ok(()); + items.sort_by(|x, y| { + if sort_err.is_ok() { + sort_compare(x, y, context).unwrap_or_else(|err| { + sort_err = Err(err); + Ordering::Equal + }) + } else { + Ordering::Equal + } + }); + sort_err?; + + // 5. Return items. + Ok(items) + } + /// Array.prototype.sort ( comparefn ) /// /// The sort method sorts the elements of an array in place and returns the sorted array. @@ -2490,113 +2696,102 @@ impl Array { } }; - // Abstract method `SortCompare`. - // - // More information: - // - [ECMAScript reference][spec] - // - // [spec]: https://tc39.es/ecma262/#sec-sortcompare - let sort_compare = - |x: &JsValue, y: &JsValue, context: &mut Context<'_>| -> JsResult { - match (x.is_undefined(), y.is_undefined()) { - // 1. If x and y are both undefined, return +0๐”ฝ. - (true, true) => return Ok(Ordering::Equal), - // 2. If x is undefined, return 1๐”ฝ. - (true, false) => return Ok(Ordering::Greater), - // 3. If y is undefined, return -1๐”ฝ. - (false, true) => return Ok(Ordering::Less), - _ => {} - } - - // 4. If comparefn is not undefined, then - if let Some(cmp) = comparefn { - let args = [x.clone(), y.clone()]; - // a. Let v be ? ToNumber(? Call(comparefn, undefined, ยซ x, y ยป)). - let v = cmp - .call(&JsValue::Undefined, &args, context)? - .to_number(context)?; - // b. If v is NaN, return +0๐”ฝ. - // c. Return v. - return Ok(v.partial_cmp(&0.0).unwrap_or(Ordering::Equal)); - } - // 5. Let xString be ? ToString(x). - // 6. Let yString be ? ToString(y). - let x_str = x.to_string(context)?; - let y_str = y.to_string(context)?; - - // 7. Let xSmaller be IsLessThan(xString, yString, true). - // 8. If xSmaller is true, return -1๐”ฝ. - // 9. Let ySmaller be IsLessThan(yString, xString, true). - // 10. If ySmaller is true, return 1๐”ฝ. - // 11. Return +0๐”ฝ. - - // NOTE: skipped IsLessThan because it just makes a lexicographic comparison - // when x and y are strings - Ok(x_str.cmp(&y_str)) - }; - // 2. Let obj be ? ToObject(this value). let obj = this.to_object(context)?; // 3. Let len be ? LengthOfArrayLike(obj). - let length = obj.length_of_array_like(context)?; + let len = obj.length_of_array_like(context)?; - // 4. Let items be a new empty List. - let mut items = Vec::with_capacity(length as usize); + // 4. Let SortCompare be a new Abstract Closure with parameters (x, y) that captures comparefn and performs the following steps when called: + let sort_compare = + |x: &JsValue, y: &JsValue, context: &mut Context<'_>| -> JsResult { + // a. Return ? CompareArrayElements(x, y, comparefn). + compare_array_elements(x, y, comparefn, context) + }; - // 5. Let k be 0. - // 6. Repeat, while k < len, - for k in 0..length { - // a. Let Pk be ! ToString(๐”ฝ(k)). - // b. Let kPresent be ? HasProperty(obj, Pk). - // c. If kPresent is true, then - if obj.has_property(k, context)? { - // i. Let kValue be ? Get(obj, Pk). - let kval = obj.get(k, context)?; - // ii. Append kValue to items. - items.push(kval); - } - // d. Set k to k + 1. - } + // 5. Let sortedList be ? SortIndexedProperties(obj, len, SortCompare, skip-holes). + let sorted = Self::sort_indexed_properties(&obj, len, sort_compare, true, context)?; - // 7. Let itemCount be the number of elements in items. - let item_count = items.len() as u64; + let sorted_len = sorted.len() as u64; - // 8. Sort items using an implementation-defined sequence of calls to SortCompare. - // If any such call returns an abrupt completion, stop before performing any further - // calls to SortCompare or steps in this algorithm and return that completion. - let mut sort_err = Ok(()); - items.sort_by(|x, y| { - if sort_err.is_ok() { - sort_compare(x, y, context).unwrap_or_else(|err| { - sort_err = Err(err); - Ordering::Equal - }) - } else { - Ordering::Equal - } - }); - sort_err?; - - // 9. Let j be 0. - // 10. Repeat, while j < itemCount, - for (j, item) in items.into_iter().enumerate() { - // a. Perform ? Set(obj, ! ToString(๐”ฝ(j)), items[j], true). + // 6. Let itemCount be the number of elements in sortedList. + // 7. Let j be 0. + // 8. Repeat, while j < itemCount, + for (j, item) in sorted.into_iter().enumerate() { + // a. Perform ? Set(obj, ! ToString(๐”ฝ(j)), sortedList[j], true). obj.set(j, item, true, context)?; + // b. Set j to j + 1. } - // 11. Repeat, while j < len, - for j in item_count..length { + // 9. NOTE: The call to SortIndexedProperties in step 5 uses skip-holes. The remaining indices + // are deleted to preserve the number of holes that were detected and excluded from the sort. + // 10. Repeat, while j < len, + for j in sorted_len..len { // a. Perform ? DeletePropertyOrThrow(obj, ! ToString(๐”ฝ(j))). obj.delete_property_or_throw(j, context)?; + // b. Set j to j + 1. } - // 12. Return obj. + // 11. Return obj. Ok(obj.into()) } + /// [`Array.prototype.toSorted ( comparefn )`][spec] + /// + /// Orders the target array, returning the result in a new array. + /// + /// [spec]: https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.tosorted + pub(crate) fn to_sorted( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. + let comparefn = match args.get_or_undefined(0) { + JsValue::Object(ref obj) if obj.is_callable() => Some(obj), + JsValue::Undefined => None, + _ => { + return Err(JsNativeError::typ() + .with_message("The comparison function must be either a function or undefined") + .into()) + } + }; + + // 2. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 3. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 4. Let A be ? ArrayCreate(len). + let arr = Array::array_create(len, None, context)?; + + // 5. Let SortCompare be a new Abstract Closure with parameters (x, y) that captures comparefn and performs the following steps when called: + let sort_compare = + |x: &JsValue, y: &JsValue, context: &mut Context<'_>| -> JsResult { + // a. Return ? CompareArrayElements(x, y, comparefn). + compare_array_elements(x, y, comparefn, context) + }; + + // 6. Let sortedList be ? SortIndexedProperties(O, len, SortCompare, read-through-holes). + let sorted = Self::sort_indexed_properties(&o, len, sort_compare, false, context)?; + + // 7. Let j be 0. + // 8. Repeat, while j < len, + for (i, item) in sorted.into_iter().enumerate() { + // a. Perform ! CreateDataPropertyOrThrow(A, ! ToString(๐”ฝ(j)), sortedList[j]). + arr.create_data_property_or_throw(i, item, context) + .expect("cannot fail for a newly created array"); + + // b. Set j to j + 1. + } + + // 9. Return A. + Ok(arr.into()) + } + /// `Array.prototype.reduce( callbackFn [ , initialValue ] )` /// /// More information: @@ -2811,19 +3006,19 @@ impl Array { // 4. If relativeTarget is -โˆž, let to be 0. // 5. Else if relativeTarget < 0, let to be max(len + relativeTarget, 0). // 6. Else, let to be min(relativeTarget, len). - let mut to = Self::get_relative_start(context, args.get(0), len)? as i64; + let mut to = Self::get_relative_start(context, args.get_or_undefined(0), len)? as i64; // 7. Let relativeStart be ? ToIntegerOrInfinity(start). // 8. If relativeStart is -โˆž, let from be 0. // 9. Else if relativeStart < 0, let from be max(len + relativeStart, 0). // 10. Else, let from be min(relativeStart, len). - let mut from = Self::get_relative_start(context, args.get(1), len)? as i64; + let mut from = Self::get_relative_start(context, args.get_or_undefined(1), len)? as i64; // 11. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). // 12. If relativeEnd is -โˆž, let final be 0. // 13. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). // 14. Else, let final be min(relativeEnd, len). - let final_ = Self::get_relative_end(context, args.get(2), len)? as i64; + let final_ = Self::get_relative_end(context, args.get_or_undefined(2), len)? as i64; // 15. Let count be min(final - from, len - to). let mut count = min(final_ - from, len as i64 - to); @@ -2954,17 +3149,78 @@ impl Array { )) } + /// [`Array.prototype.with ( index, value )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.with + pub(crate) fn with( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. Let relativeIndex be ? ToIntegerOrInfinity(index). + let IntegerOrInfinity::Integer(relative_index) = + args.get_or_undefined(0).to_integer_or_infinity(context)? + else { + return Err(JsNativeError::range() + .with_message("invalid integer index for TypedArray operation") + .into()); + }; + + let value = args.get_or_undefined(1); + + // 4. If relativeIndex โ‰ฅ 0, let actualIndex be relativeIndex. + let actual_index = u64::try_from(relative_index) // should succeed if `relative_index >= 0` + .ok() + // 5. Else, let actualIndex be len + relativeIndex. + .or_else(|| len.checked_add_signed(relative_index)) + .filter(|&rel| rel < len) + .ok_or_else(|| { + // 6. If actualIndex โ‰ฅ len or actualIndex < 0, throw a RangeError exception. + JsNativeError::range() + .with_message("invalid integer index for TypedArray operation") + })?; + + // 7. Let A be ? ArrayCreate(len). + let new_array = Array::array_create(len, None, context)?; + + // 8. Let k be 0. + // 9. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(๐”ฝ(k)). + let from_value = if k == actual_index { + // b. If k is actualIndex, let fromValue be value. + value.clone() + } else { + // c. Else, let fromValue be ? Get(O, Pk). + o.get(k, context)? + }; + + // d. Perform ! CreateDataPropertyOrThrow(A, Pk, fromValue). + new_array + .create_data_property_or_throw(k, from_value, context) + .expect("cannot fail for a newly created array"); + + // e. Set k to k + 1. + } + + // 10. Return A. + Ok(new_array.into()) + } + /// Represents the algorithm to calculate `relativeStart` (or `k`) in array functions. pub(super) fn get_relative_start( context: &mut Context<'_>, - arg: Option<&JsValue>, + arg: &JsValue, len: u64, ) -> JsResult { // 1. Let relativeStart be ? ToIntegerOrInfinity(start). - let relative_start = arg - .cloned() - .unwrap_or_default() - .to_integer_or_infinity(context)?; + let relative_start = arg.to_integer_or_infinity(context)?; match relative_start { // 2. If relativeStart is -โˆž, let k be 0. IntegerOrInfinity::NegativeInfinity => Ok(0), @@ -2982,11 +3238,9 @@ impl Array { /// Represents the algorithm to calculate `relativeEnd` (or `final`) in array functions. pub(super) fn get_relative_end( context: &mut Context<'_>, - arg: Option<&JsValue>, + value: &JsValue, len: u64, ) -> JsResult { - let default_value = JsValue::undefined(); - let value = arg.unwrap_or(&default_value); // 1. If end is undefined, let relativeEnd be len [and return it] if value.is_undefined() { Ok(len) @@ -3065,6 +3319,53 @@ impl Array { } } +/// [`CompareArrayElements ( x, y, comparefn )`][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-comparearrayelements +fn compare_array_elements( + x: &JsValue, + y: &JsValue, + comparefn: Option<&JsObject>, + context: &mut Context<'_>, +) -> JsResult { + match (x.is_undefined(), y.is_undefined()) { + // 1. If x and y are both undefined, return +0๐”ฝ. + (true, true) => return Ok(Ordering::Equal), + // 2. If x is undefined, return 1๐”ฝ. + (true, false) => return Ok(Ordering::Greater), + // 3. If y is undefined, return -1๐”ฝ. + (false, true) => return Ok(Ordering::Less), + _ => {} + } + + // 4. If comparefn is not undefined, then + if let Some(cmp) = comparefn { + let args = [x.clone(), y.clone()]; + // a. Let v be ? ToNumber(? Call(comparefn, undefined, ยซ x, y ยป)). + let v = cmp + .call(&JsValue::Undefined, &args, context)? + .to_number(context)?; + // b. If v is NaN, return +0๐”ฝ. + // c. Return v. + return Ok(v.partial_cmp(&0.0).unwrap_or(Ordering::Equal)); + } + + // 5. Let xString be ? ToString(x). + let x_str = x.to_string(context)?; + + // 6. Let yString be ? ToString(y). + let y_str = y.to_string(context)?; + + // 7. Let xSmaller be ! IsLessThan(xString, yString, true). + // 8. If xSmaller is true, return -1๐”ฝ. + // 9. Let ySmaller be ! IsLessThan(yString, xString, true). + // 10. If ySmaller is true, return 1๐”ฝ. + // 11. Return +0๐”ฝ. + // NOTE: skipped IsLessThan because it just makes a lexicographic comparison + // when x and y are strings + Ok(x_str.cmp(&y_str)) +} + /// `FindViaPredicate ( O, len, direction, predicate, thisArg )` /// /// More information: diff --git a/boa_engine/src/builtins/array/tests.rs b/boa_engine/src/builtins/array/tests.rs index 595f4d333c..bef707afeb 100644 --- a/boa_engine/src/builtins/array/tests.rs +++ b/boa_engine/src/builtins/array/tests.rs @@ -874,6 +874,7 @@ fn array_spread_non_iterable() { fn get_relative_start() { #[track_caller] fn assert(context: &mut Context<'_>, arg: Option<&JsValue>, len: u64, expected: u64) { + let arg = arg.unwrap_or(&JsValue::Undefined); assert_eq!( Array::get_relative_start(context, arg, len).unwrap(), expected @@ -900,6 +901,7 @@ fn get_relative_start() { fn get_relative_end() { #[track_caller] fn assert(context: &mut Context<'_>, arg: Option<&JsValue>, len: u64, expected: u64) { + let arg = arg.unwrap_or(&JsValue::Undefined); assert_eq!( Array::get_relative_end(context, arg, len).unwrap(), expected diff --git a/boa_engine/src/builtins/typed_array/builtin.rs b/boa_engine/src/builtins/typed_array/builtin.rs index eaa05703b8..ebb1911016 100644 --- a/boa_engine/src/builtins/typed_array/builtin.rs +++ b/boa_engine/src/builtins/typed_array/builtin.rs @@ -1,5 +1,6 @@ use std::{cmp::Ordering, ptr, sync::atomic}; +use boa_gc::GcRef; use boa_macros::utf16; use num_traits::Zero; @@ -11,13 +12,13 @@ use crate::{ ArrayBuffer, BufferRef, }, iterable::iterable_to_list, - BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, object::{ internal_methods::{get_prototype_from_constructor, integer_indexed_element_set}, - ObjectData, + Object, ObjectData, }, property::{Attribute, PropertyNameKind}, realm::Realm, @@ -135,6 +136,9 @@ impl IntrinsicObject for BuiltinTypedArray { .method(Self::sort, js_string!("sort"), 1) .method(Self::subarray, js_string!("subarray"), 2) .method(Self::to_locale_string, js_string!("toLocaleString"), 0) + .method(Self::to_reversed, js_string!("toReversed"), 0) + .method(Self::to_sorted, js_string!("toSorted"), 1) + .method(Self::with, js_string!("with"), 2) // 23.2.3.29 %TypedArray%.prototype.toString ( ) // The initial value of the %TypedArray%.prototype.toString data property is the same // built-in function object as the Array.prototype.toString method defined in 23.1.3.30. @@ -291,6 +295,20 @@ impl BuiltinTypedArray { Ok(target_obj.into()) } + /// [`TypedArrayCreateSameType ( exemplar, argumentList )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-typedarray-create-same-type + fn from_kind_and_length( + kind: TypedArrayKind, + length: u64, + context: &mut Context<'_>, + ) -> JsResult { + let constructor = + kind.standard_constructor()(context.intrinsics().constructors()).constructor(); + + Self::create(&constructor, &[length.into()], context) + } + /// `23.2.2.2 %TypedArray%.of ( ...items )` /// /// More information: @@ -1823,19 +1841,19 @@ impl BuiltinTypedArray { } // 3. Let len be O.[[ArrayLength]]. - let len = o.array_length() as f64; + let len = o.array_length(); drop(obj_borrow); // 4. Let middle be floor(len / 2). - let middle = (len / 2.0).floor(); + let middle = len / 2; // 5. Let lower be 0. - let mut lower = 0.0; + let mut lower = 0; // 6. Repeat, while lower โ‰  middle, while lower != middle { // a. Let upper be len - lower - 1. - let upper = len - lower - 1.0; + let upper = len - lower - 1; // b. Let upperP be ! ToString(๐”ฝ(upper)). // c. Let lowerP be ! ToString(๐”ฝ(lower)). @@ -1852,13 +1870,64 @@ impl BuiltinTypedArray { .expect("Set cannot fail here"); // h. Set lower to lower + 1. - lower += 1.0; + lower += 1; } // 7. Return O. Ok(this.clone()) } + /// [`%TypedArray%.prototype.toReversed ( )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.toreversed + pub(crate) fn to_reversed( + this: &JsValue, + _: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Let iieoRecord be ? ValidateTypedArray(O, seq-cst). + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Value is not a typed array object") + })?; + let array = GcRef::try_map(obj.borrow(), Object::as_typed_array).ok_or_else(|| { + JsNativeError::typ().with_message("Value is not a typed array object") + })?; + if array.is_detached() { + return Err(JsNativeError::typ() + .with_message("Buffer of the typed array is detached") + .into()); + } + + // 3. Let length be IntegerIndexedObjectLength(iieoRecord). + let length = array.array_length(); + + // 4. Let A be ? TypedArrayCreateSameType(O, ยซ ๐”ฝ(length) ยป). + let new_array = Self::from_kind_and_length(array.kind(), length, context)?; + + drop(array); + + // 5. Let k be 0. + // 6. Repeat, while k < length, + for k in 0..length { + // a. Let from be ! ToString(๐”ฝ(length - k - 1)). + // b. Let Pk be ! ToString(๐”ฝ(k)). + // c. Let fromValue be ! Get(O, from). + let value = obj + .get(length - k - 1, context) + .expect("cannot fail per the spec"); + // d. Perform ! Set(A, Pk, fromValue, true). + new_array + .set(k, value, true, context) + .expect("cannot fail per the spec"); + // e. Set k to k + 1. + } + + // 7. Return A. + + Ok(new_array.into()) + } + /// `23.2.3.24 %TypedArray%.prototype.set ( source [ , offset ] )` /// /// More information: @@ -2489,12 +2558,13 @@ impl BuiltinTypedArray { }; // 2. Let obj be the this value. - // 3. Perform ? ValidateTypedArray(obj). - // 4. Let len be obj.[[ArrayLength]]. + // 3. Let iieoRecord be ? ValidateTypedArray(obj, seq-cst). let obj = this.as_object().ok_or_else(|| { JsNativeError::typ() .with_message("TypedArray.sort must be called on typed array object") })?; + + // 4. Let len be IntegerIndexedObjectLength(iieoRecord). let len = { let obj_borrow = obj.borrow(); @@ -2513,56 +2583,18 @@ impl BuiltinTypedArray { // 5. NOTE: The following closure performs a numeric comparison rather than the string comparison used in 23.1.3.30. // 6. Let SortCompare be a new Abstract Closure with parameters (x, y) that captures comparefn and performs the following steps when called: - let sort_compare = |x: &JsValue, - y: &JsValue, - compare_fn: Option<&JsObject>, - context: &mut Context<'_>| - -> JsResult { - // a. Return ? CompareTypedArrayElements(x, y, comparefn). - compare_typed_array_elements(x, y, compare_fn, context) - }; + let sort_compare = + |x: &JsValue, y: &JsValue, context: &mut Context<'_>| -> JsResult { + // a. Return ? CompareTypedArrayElements(x, y, comparefn). + compare_typed_array_elements(x, y, compare_fn, context) + }; - // Note: This step is currently inlined. // 7. Let sortedList be ? SortIndexedProperties(obj, len, SortCompare, read-through-holes). - // 1. Let items be a new empty List. - let mut items = Vec::with_capacity(len as usize); - - // 2. Let k be 0. - // 3. Repeat, while k < len, - for k in 0..len { - // a. Let Pk be ! ToString(๐”ฝ(k)). - // b. If holes is skip-holes, then - // i. Let kRead be ? HasProperty(obj, Pk). - // c. Else, - // i. Assert: holes is read-through-holes. - // ii. Let kRead be true. - // d. If kRead is true, then - // i. Let kValue be ? Get(obj, Pk). - let k_value = obj.get(k, context)?; - - // ii. Append kValue to items. - items.push(k_value); - // e. Set k to k + 1. - } - - // 4. Sort items using an implementation-defined sequence of calls to SortCompare. If any such call returns an abrupt completion, stop before performing any further calls to SortCompare and return that Completion Record. - // 5. Return items. - let mut sort_err = Ok(()); - items.sort_by(|x, y| { - if sort_err.is_ok() { - sort_compare(x, y, compare_fn, context).unwrap_or_else(|err| { - sort_err = Err(err); - Ordering::Equal - }) - } else { - Ordering::Equal - } - }); - sort_err?; + let sorted = Array::sort_indexed_properties(obj, len, sort_compare, false, context)?; // 8. Let j be 0. // 9. Repeat, while j < len, - for (j, item) in items.into_iter().enumerate() { + for (j, item) in sorted.into_iter().enumerate() { // a. Perform ! Set(obj, ! ToString(๐”ฝ(j)), sortedList[j], true). obj.set(j, item, true, context) .expect("cannot fail per spec"); @@ -2574,6 +2606,74 @@ impl BuiltinTypedArray { Ok(obj.clone().into()) } + /// [`%TypedArray%.prototype.toSorted ( comparefn )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.tosorted + pub(crate) fn to_sorted( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. + let compare_fn = match args.get(0) { + None | Some(JsValue::Undefined) => None, + Some(JsValue::Object(obj)) if obj.is_callable() => Some(obj), + _ => { + return Err(JsNativeError::typ() + .with_message("TypedArray.sort called with non-callable comparefn") + .into()) + } + }; + + // 2. Let O be the this value. + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ() + .with_message("TypedArray.sort must be called on typed array object") + })?; + + // 3. Let iieoRecord be ? ValidateTypedArray(O, seq-cst). + let array = GcRef::try_map(obj.borrow(), Object::as_typed_array).ok_or_else(|| { + JsNativeError::typ().with_message("Value is not a typed array object") + })?; + if array.is_detached() { + return Err(JsNativeError::typ() + .with_message("Buffer of the typed array is detached") + .into()); + } + + // 4. Let len be IntegerIndexedObjectLength(iieoRecord). + let len = array.array_length(); + + // 5. Let A be ? TypedArrayCreateSameType(O, ยซ ๐”ฝ(len) ยป). + let new_array = Self::from_kind_and_length(array.kind(), len, context)?; + + drop(array); + + // 6. NOTE: The following closure performs a numeric comparison rather than the string comparison used in 23.1.3.34. + // 7. Let SortCompare be a new Abstract Closure with parameters (x, y) that captures comparefn and performs the following steps when called: + let sort_compare = + |x: &JsValue, y: &JsValue, context: &mut Context<'_>| -> JsResult { + // a. Return ? CompareTypedArrayElements(x, y, comparefn). + compare_typed_array_elements(x, y, compare_fn, context) + }; + + // 8. Let sortedList be ? SortIndexedProperties(O, len, SortCompare, read-through-holes). + let sorted = Array::sort_indexed_properties(obj, len, sort_compare, false, context)?; + + // 9. Let j be 0. + // 10. Repeat, while j < len + for (j, item) in sorted.into_iter().enumerate() { + // a. Perform ! Set(A, ! ToString(๐”ฝ(j)), sortedList[j], true). + new_array + .set(j, item, true, context) + .expect("cannot fail per spec"); + + // b. Set j to j + 1. + } + // 11. Return A. + Ok(new_array.into()) + } + /// `23.2.3.28 %TypedArray%.prototype.subarray ( begin, end )` /// /// More information: @@ -2779,6 +2879,99 @@ impl BuiltinTypedArray { )) } + /// [`%TypedArray%.prototype.with ( index, value )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.with + pub(crate) fn with( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let O be the this value. + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ() + .with_message("TypedArray.sort must be called on typed array object") + })?; + + // 2. Let iieoRecord be ? ValidateTypedArray(O, seq-cst). + let array = GcRef::try_map(obj.borrow(), Object::as_typed_array).ok_or_else(|| { + JsNativeError::typ().with_message("Value is not a typed array object") + })?; + if array.is_detached() { + return Err(JsNativeError::typ() + .with_message("Buffer of the typed array is detached") + .into()); + } + + // 3. Let len be IntegerIndexedObjectLength(iieoRecord). + let len = array.array_length(); + let kind = array.kind(); + + drop(array); + + // 4. Let relativeIndex be ? ToIntegerOrInfinity(index). + // triggers any conversion errors before throwing range errors. + let relative_index = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + let value = args.get_or_undefined(1); + + // 7. If O.[[ContentType]] is bigint, let numericValue be ? ToBigInt(value). + let numeric_value: JsValue = if kind.content_type() == ContentType::BigInt { + value.to_bigint(context)?.into() + } else { + // 8. Else, let numericValue be ? ToNumber(value). + value.to_number(context)?.into() + }; + + // 9. If IsValidIntegerIndex(O, ๐”ฝ(actualIndex)) is false, throw a RangeError exception. + let IntegerOrInfinity::Integer(relative_index) = relative_index else { + return Err(JsNativeError::range() + .with_message("invalid integer index for TypedArray operation") + .into()); + }; + + let actual_index = u64::try_from(relative_index) // should succeed if `relative_index >= 0` + .ok() + .or_else(|| len.checked_add_signed(relative_index)) + .filter(|&rel| rel < len) + .ok_or_else(|| { + JsNativeError::range() + .with_message("invalid integer index for TypedArray operation") + })?; + + // TODO: Replace with `is_valid_integer_index_u64` or equivalent. + if !is_valid_integer_index(obj, actual_index as f64) { + return Err(JsNativeError::range() + .with_message("invalid integer index for TypedArray operation") + .into()); + } + + // 10. Let A be ? TypedArrayCreateSameType(O, ยซ ๐”ฝ(len) ยป). + let new_array = Self::from_kind_and_length(kind, len, context)?; + + // 11. Let k be 0. + // 12. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(๐”ฝ(k)). + let value = if k == actual_index { + // b. If k is actualIndex, let fromValue be numericValue. + numeric_value.clone() + } else { + // c. Else, let fromValue be ! Get(O, Pk). + obj.get(k, context).expect("cannot fail per the spec") + }; + // d. Perform ! Set(A, Pk, fromValue, true). + new_array + .set(k, value, true, context) + .expect("cannot fail per the spec"); + + // e. Set k to k + 1. + } + + // 13. Return A. + Ok(new_array.into()) + } + /// `23.2.3.33 get %TypedArray%.prototype [ @@toStringTag ]` /// /// More information: @@ -3291,6 +3484,12 @@ impl BuiltinTypedArray { } } +#[derive(Debug)] +enum U64OrPositiveInfinity { + U64(u64), + PositiveInfinity, +} + /// `CompareTypedArrayElements ( x, y, comparefn )` /// /// More information: @@ -3382,7 +3581,38 @@ fn compare_typed_array_elements( } } -enum U64OrPositiveInfinity { - U64(u64), - PositiveInfinity, +/// Abstract operation `IsValidIntegerIndex ( O, index )`. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-isvalidintegerindex +pub(crate) fn is_valid_integer_index(obj: &JsObject, index: f64) -> bool { + let obj = obj.borrow(); + let inner = obj.as_typed_array().expect( + "integer indexed exotic method should only be callable from integer indexed objects", + ); + + // 1. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is true, return false. + if inner.is_detached() { + return false; + } + + // 2. If IsIntegralNumber(index) is false, return false. + if index.is_nan() || index.is_infinite() || index.fract() != 0.0 { + return false; + } + + // 3. If index is -0๐”ฝ, return false. + if index == 0.0 && index.is_sign_negative() { + return false; + } + + // 4. If โ„(index) < 0 or โ„(index) โ‰ฅ O.[[ArrayLength]], return false. + if index < 0.0 || index >= inner.array_length() as f64 { + return false; + } + + // 5. Return true. + true } diff --git a/boa_engine/src/builtins/typed_array/mod.rs b/boa_engine/src/builtins/typed_array/mod.rs index 3d0af35c55..82d751a1c2 100644 --- a/boa_engine/src/builtins/typed_array/mod.rs +++ b/boa_engine/src/builtins/typed_array/mod.rs @@ -34,7 +34,7 @@ mod builtin; mod element; mod integer_indexed_object; -pub(crate) use builtin::BuiltinTypedArray; +pub(crate) use builtin::{is_valid_integer_index, BuiltinTypedArray}; pub(crate) use element::{ClampedU8, Element}; pub use integer_indexed_object::IntegerIndexed; diff --git a/boa_engine/src/object/builtins/jsarray.rs b/boa_engine/src/object/builtins/jsarray.rs index 9433b88167..d5eaa59599 100644 --- a/boa_engine/src/object/builtins/jsarray.rs +++ b/boa_engine/src/object/builtins/jsarray.rs @@ -51,7 +51,7 @@ impl JsArray { /// Get the length of the array. /// - /// Same a `array.length` in JavaScript. + /// Same as `array.length` in JavaScript. #[inline] pub fn length(&self, context: &mut Context<'_>) -> JsResult { self.inner.length_of_array_like(context) @@ -373,6 +373,53 @@ impl JsArray { context, ) } + + /// Calls `Array.prototype.toReversed`. + #[inline] + pub fn to_reversed(&self, context: &mut Context<'_>) -> JsResult { + let array = Array::to_reversed(&self.inner.clone().into(), &[], context)?; + + Ok(Self { + inner: array + .as_object() + .cloned() + .expect("`to_reversed` must always return an `Array` on success"), + }) + } + + /// Calls `Array.prototype.toSorted`. + #[inline] + pub fn to_sorted( + &self, + compare_fn: Option, + context: &mut Context<'_>, + ) -> JsResult { + let array = Array::to_sorted( + &self.inner.clone().into(), + &[compare_fn.into_or_undefined()], + context, + )?; + + Ok(Self { + inner: array + .as_object() + .cloned() + .expect("`to_sorted` must always return an `Array` on success"), + }) + } + + /// Calls `Array.prototype.with`. + #[inline] + pub fn with(&self, index: u64, value: JsValue, context: &mut Context<'_>) -> JsResult { + let array = Array::with(&self.inner.clone().into(), &[index.into(), value], context)?; + + Ok(Self { + inner: array + .as_object() + .cloned() + .expect("`with` must always return an `Array` on success"), + }) + } } impl From for JsObject { diff --git a/boa_engine/src/object/builtins/jstypedarray.rs b/boa_engine/src/object/builtins/jstypedarray.rs index 947192edeb..e9ccf9e368 100644 --- a/boa_engine/src/object/builtins/jstypedarray.rs +++ b/boa_engine/src/object/builtins/jstypedarray.rs @@ -14,7 +14,7 @@ use std::ops::Deref; /// builtin object. #[derive(Debug, Clone, Trace, Finalize)] pub struct JsTypedArray { - inner: JsValue, + inner: JsObject, } impl JsTypedArray { @@ -26,9 +26,7 @@ impl JsTypedArray { #[inline] pub fn from_object(object: JsObject) -> JsResult { if object.is_typed_array() { - Ok(Self { - inner: object.into(), - }) + Ok(Self { inner: object }) } else { Err(JsNativeError::typ() .with_message("object is not a TypedArray") @@ -38,13 +36,15 @@ impl JsTypedArray { /// Get the length of the array. /// - /// Same a `array.length` in JavaScript. + /// Same as `array.length` in JavaScript. #[inline] pub fn length(&self, context: &mut Context<'_>) -> JsResult { - Ok(BuiltinTypedArray::length(&self.inner, &[], context)? - .as_number() - .map(|x| x as usize) - .expect("length should return a number")) + Ok( + BuiltinTypedArray::length(&self.inner.clone().into(), &[], context)? + .as_number() + .map(|x| x as usize) + .expect("length should return a number"), + ) } /// Check if the array is empty, i.e. the `length` is zero. @@ -58,25 +58,29 @@ impl JsTypedArray { where T: Into, { - BuiltinTypedArray::at(&self.inner, &[index.into().into()], context) + BuiltinTypedArray::at(&self.inner.clone().into(), &[index.into().into()], context) } /// Returns `TypedArray.prototype.byteLength`. #[inline] pub fn byte_length(&self, context: &mut Context<'_>) -> JsResult { - Ok(BuiltinTypedArray::byte_length(&self.inner, &[], context)? - .as_number() - .map(|x| x as usize) - .expect("byteLength should return a number")) + Ok( + BuiltinTypedArray::byte_length(&self.inner.clone().into(), &[], context)? + .as_number() + .map(|x| x as usize) + .expect("byteLength should return a number"), + ) } /// Returns `TypedArray.prototype.byteOffset`. #[inline] pub fn byte_offset(&self, context: &mut Context<'_>) -> JsResult { - Ok(BuiltinTypedArray::byte_offset(&self.inner, &[], context)? - .as_number() - .map(|x| x as usize) - .expect("byteLength should return a number")) + Ok( + BuiltinTypedArray::byte_offset(&self.inner.clone().into(), &[], context)? + .as_number() + .map(|x| x as usize) + .expect("byteLength should return a number"), + ) } /// Calls `TypedArray.prototype.fill()`. @@ -91,7 +95,7 @@ impl JsTypedArray { T: Into, { BuiltinTypedArray::fill( - &self.inner, + &self.inner.clone().into(), &[ value.into(), start.into_or_undefined(), @@ -110,7 +114,7 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { let result = BuiltinTypedArray::every( - &self.inner, + &self.inner.clone().into(), &[predicate.into(), this_arg.into_or_undefined()], context, )? @@ -129,7 +133,7 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { let result = BuiltinTypedArray::some( - &self.inner, + &self.inner.clone().into(), &[callback.into(), this_arg.into_or_undefined()], context, )? @@ -146,7 +150,11 @@ impl JsTypedArray { compare_fn: Option, context: &mut Context<'_>, ) -> JsResult { - BuiltinTypedArray::sort(&self.inner, &[compare_fn.into_or_undefined()], context)?; + BuiltinTypedArray::sort( + &self.inner.clone().into(), + &[compare_fn.into_or_undefined()], + context, + )?; Ok(self.clone()) } @@ -160,12 +168,17 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { let object = BuiltinTypedArray::filter( - &self.inner, + &self.inner.clone().into(), &[callback.into(), this_arg.into_or_undefined()], context, )?; - Ok(Self { inner: object }) + Ok(Self { + inner: object + .as_object() + .cloned() + .expect("`filter` must always return a `TypedArray` on success"), + }) } /// Calls `TypedArray.prototype.map()`. @@ -177,12 +190,17 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { let object = BuiltinTypedArray::map( - &self.inner, + &self.inner.clone().into(), &[callback.into(), this_arg.into_or_undefined()], context, )?; - Ok(Self { inner: object }) + Ok(Self { + inner: object + .as_object() + .cloned() + .expect("`map` must always return a `TypedArray` on success"), + }) } /// Calls `TypedArray.prototype.reduce()`. @@ -194,7 +212,7 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { BuiltinTypedArray::reduce( - &self.inner, + &self.inner.clone().into(), &[callback.into(), initial_value.into_or_undefined()], context, ) @@ -209,7 +227,7 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { BuiltinTypedArray::reduceright( - &self.inner, + &self.inner.clone().into(), &[callback.into(), initial_value.into_or_undefined()], context, ) @@ -218,7 +236,7 @@ impl JsTypedArray { /// Calls `TypedArray.prototype.reverse()`. #[inline] pub fn reverse(&self, context: &mut Context<'_>) -> JsResult { - BuiltinTypedArray::reverse(&self.inner, &[], context)?; + BuiltinTypedArray::reverse(&self.inner.clone().into(), &[], context)?; Ok(self.clone()) } @@ -231,12 +249,17 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { let object = BuiltinTypedArray::slice( - &self.inner, + &self.inner.clone().into(), &[start.into_or_undefined(), end.into_or_undefined()], context, )?; - Ok(Self { inner: object }) + Ok(Self { + inner: object + .as_object() + .cloned() + .expect("`slice` must always return a `TypedArray` on success"), + }) } /// Calls `TypedArray.prototype.find()`. @@ -248,7 +271,7 @@ impl JsTypedArray { context: &mut Context<'_>, ) -> JsResult { BuiltinTypedArray::find( - &self.inner, + &self.inner.clone().into(), &[predicate.into(), this_arg.into_or_undefined()], context, ) @@ -265,7 +288,7 @@ impl JsTypedArray { T: Into, { let index = BuiltinTypedArray::index_of( - &self.inner, + &self.inner.clone().into(), &[search_element.into(), from_index.into_or_undefined()], context, )? @@ -291,7 +314,7 @@ impl JsTypedArray { T: Into, { let index = BuiltinTypedArray::last_index_of( - &self.inner, + &self.inner.clone().into(), &[search_element.into(), from_index.into_or_undefined()], context, )? @@ -313,28 +336,78 @@ impl JsTypedArray { separator: Option, context: &mut Context<'_>, ) -> JsResult { - BuiltinTypedArray::join(&self.inner, &[separator.into_or_undefined()], context).map(|x| { + BuiltinTypedArray::join( + &self.inner.clone().into(), + &[separator.into_or_undefined()], + context, + ) + .map(|x| { x.as_string() .cloned() .expect("TypedArray.prototype.join always returns string") }) } + + /// Calls `TypedArray.prototype.toReversed ( )`. + #[inline] + pub fn to_reversed(&self, context: &mut Context<'_>) -> JsResult { + let array = BuiltinTypedArray::to_reversed(&self.inner.clone().into(), &[], context)?; + + Ok(Self { + inner: array + .as_object() + .cloned() + .expect("`to_reversed` must always return a `TypedArray` on success"), + }) + } + + /// Calls `TypedArray.prototype.toSorted ( comparefn )`. + #[inline] + pub fn to_sorted( + &self, + compare_fn: Option, + context: &mut Context<'_>, + ) -> JsResult { + let array = BuiltinTypedArray::to_sorted( + &self.inner.clone().into(), + &[compare_fn.into_or_undefined()], + context, + )?; + + Ok(Self { + inner: array + .as_object() + .cloned() + .expect("`to_sorted` must always return a `TypedArray` on success"), + }) + } + + /// Calls `TypedArray.prototype.with ( index, value )`. + #[inline] + pub fn with(&self, index: u64, value: JsValue, context: &mut Context<'_>) -> JsResult { + let array = + BuiltinTypedArray::with(&self.inner.clone().into(), &[index.into(), value], context)?; + + Ok(Self { + inner: array + .as_object() + .cloned() + .expect("`with` must always return a `TypedArray` on success"), + }) + } } impl From for JsObject { #[inline] fn from(o: JsTypedArray) -> Self { - o.inner - .as_object() - .expect("should always be an object") - .clone() + o.inner.clone() } } impl From for JsValue { #[inline] fn from(o: JsTypedArray) -> Self { - o.inner.clone() + o.inner.clone().into() } } @@ -343,7 +416,7 @@ impl Deref for JsTypedArray { #[inline] fn deref(&self) -> &Self::Target { - self.inner.as_object().expect("should always be an object") + &self.inner } } @@ -463,8 +536,6 @@ macro_rules! JsTypedArrayType { fn from(o: $name) -> Self { o.inner .inner - .as_object() - .expect("should always be an object") .clone() } } @@ -472,7 +543,7 @@ macro_rules! JsTypedArrayType { impl From<$name> for JsValue { #[inline] fn from(o: $name) -> Self { - o.inner.inner.clone() + o.inner.inner.clone().into() } } diff --git a/boa_engine/src/object/internal_methods/integer_indexed.rs b/boa_engine/src/object/internal_methods/integer_indexed.rs index 3a9f95fa62..a0c9156213 100644 --- a/boa_engine/src/object/internal_methods/integer_indexed.rs +++ b/boa_engine/src/object/internal_methods/integer_indexed.rs @@ -3,7 +3,7 @@ use std::sync::atomic; use boa_macros::utf16; use crate::{ - builtins::Number, + builtins::{typed_array::is_valid_integer_index, Number}, object::JsObject, property::{PropertyDescriptor, PropertyKey}, Context, JsResult, JsString, JsValue, @@ -331,42 +331,6 @@ pub(crate) fn integer_indexed_exotic_own_property_keys( Ok(keys) } -/// Abstract operation `IsValidIntegerIndex ( O, index )`. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// -/// [spec]: https://tc39.es/ecma262/#sec-isvalidintegerindex -pub(crate) fn is_valid_integer_index(obj: &JsObject, index: f64) -> bool { - let obj = obj.borrow(); - let inner = obj.as_typed_array().expect( - "integer indexed exotic method should only be callable from integer indexed objects", - ); - - // 1. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is true, return false. - if inner.is_detached() { - return false; - } - - // 2. If IsIntegralNumber(index) is false, return false. - if index.is_nan() || index.is_infinite() || index.fract() != 0.0 { - return false; - } - - // 3. If index is -0๐”ฝ, return false. - if index == 0.0 && index.is_sign_negative() { - return false; - } - - // 4. If โ„(index) < 0 or โ„(index) โ‰ฅ O.[[ArrayLength]], return false. - if index < 0.0 || index >= inner.array_length() as f64 { - return false; - } - - // 5. Return true. - true -} - /// Abstract operation `IntegerIndexedElementGet ( O, index )`. /// /// More information: diff --git a/test262_config.toml b/test262_config.toml index d5c25d7d0d..da51ee0273 100644 --- a/test262_config.toml +++ b/test262_config.toml @@ -10,7 +10,7 @@ features = [ "FinalizationRegistry", "IsHTMLDDA", "Atomics", - "change-array-by-copy", + "resizable-arraybuffer", "symbols-as-weakmap-keys", "intl-normative-optional", "Intl.DisplayNames", @@ -32,9 +32,6 @@ features = [ # https://github.com/tc39/proposal-json-modules "json-modules", - # https://github.com/tc39/proposal-resizablearraybuffer - "resizable-arraybuffer", - # https://github.com/tc39/proposal-arraybuffer-transfer "arraybuffer-transfer",