mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1201 lines
44 KiB
1201 lines
44 KiB
//! This module implements the global `Array` object. |
|
//! |
|
//! The JavaScript `Array` class is a global object that is used in the construction of arrays; which are high-level, list-like objects. |
|
//! |
|
//! More information: |
|
//! - [ECMAScript reference][spec] |
|
//! - [MDN documentation][mdn] |
|
//! |
|
//! [spec]: https://tc39.es/ecma262/#sec-array-objects |
|
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array |
|
|
|
#[cfg(test)] |
|
mod tests; |
|
|
|
use super::function::{make_builtin_fn, make_constructor_fn}; |
|
use crate::{ |
|
builtins::{ |
|
object::{ObjectData, PROTOTYPE}, |
|
property::{Attribute, Property}, |
|
value::{same_value_zero, Value}, |
|
}, |
|
exec::Interpreter, |
|
BoaProfiler, Result, |
|
}; |
|
use std::{ |
|
borrow::Borrow, |
|
cmp::{max, min}, |
|
}; |
|
|
|
/// JavaScript `Array` built-in implementation. |
|
#[derive(Debug, Clone, Copy)] |
|
pub(crate) struct Array; |
|
|
|
impl Array { |
|
/// The name of the object. |
|
pub(crate) const NAME: &'static str = "Array"; |
|
|
|
/// The amount of arguments this function object takes. |
|
pub(crate) const LENGTH: usize = 1; |
|
|
|
/// Creates a new `Array` instance. |
|
pub(crate) fn new_array(interpreter: &Interpreter) -> Result<Value> { |
|
let array = Value::new_object(Some( |
|
&interpreter |
|
.realm() |
|
.environment |
|
.get_global_object() |
|
.expect("Could not get global object"), |
|
)); |
|
array.set_data(ObjectData::Array); |
|
array.as_object_mut().expect("array object").set_prototype( |
|
interpreter |
|
.realm() |
|
.environment |
|
.get_binding_value("Array") |
|
.expect("Array was not initialized") |
|
.borrow() |
|
.get_field(PROTOTYPE), |
|
); |
|
array.borrow().set_field("length", Value::from(0)); |
|
Ok(array) |
|
} |
|
|
|
/// Utility function for creating array objects. |
|
/// |
|
/// `array_obj` can be any array with prototype already set (it will be wiped and |
|
/// recreated from `array_contents`) |
|
pub(crate) fn construct_array(array_obj: &Value, array_contents: &[Value]) -> Result<Value> { |
|
let array_obj_ptr = array_obj.clone(); |
|
|
|
// Wipe existing contents of the array object |
|
let orig_length = array_obj.get_field("length").as_number().unwrap() as i32; |
|
for n in 0..orig_length { |
|
array_obj_ptr.remove_property(n); |
|
} |
|
|
|
// Create length |
|
let length = Property::data_descriptor( |
|
array_contents.len().into(), |
|
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, |
|
); |
|
array_obj_ptr.set_property("length".to_string(), length); |
|
|
|
for (n, value) in array_contents.iter().enumerate() { |
|
array_obj_ptr.set_field(n, value); |
|
} |
|
Ok(array_obj_ptr) |
|
} |
|
|
|
/// Utility function which takes an existing array object and puts additional |
|
/// values on the end, correctly rewriting the length |
|
pub(crate) fn add_to_array_object(array_ptr: &Value, add_values: &[Value]) -> Result<Value> { |
|
let orig_length = array_ptr.get_field("length").as_number().unwrap() as i32; |
|
|
|
for (n, value) in add_values.iter().enumerate() { |
|
let new_index = orig_length.wrapping_add(n as i32); |
|
array_ptr.set_field(new_index, value); |
|
} |
|
|
|
array_ptr.set_field( |
|
"length", |
|
Value::from(orig_length.wrapping_add(add_values.len() as i32)), |
|
); |
|
|
|
Ok(array_ptr.clone()) |
|
} |
|
|
|
/// Create a new array |
|
pub(crate) fn make_array(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result<Value> { |
|
// Make a new Object which will internally represent the Array (mapping |
|
// between indices and values): this creates an Object with no prototype |
|
|
|
// Set Prototype |
|
let prototype = ctx.realm.global_obj.get_field("Array").get_field(PROTOTYPE); |
|
|
|
this.as_object_mut() |
|
.expect("this should be an array object") |
|
.set_prototype(prototype); |
|
// This value is used by console.log and other routines to match Object type |
|
// to its Javascript Identifier (global constructor method name) |
|
this.set_data(ObjectData::Array); |
|
|
|
// add our arguments in |
|
let mut length = args.len() as i32; |
|
match args.len() { |
|
1 if args[0].is_integer() => { |
|
length = args[0].as_number().unwrap() as i32; |
|
// TODO: It should not create an array of undefineds, but an empty array ("holy" array in V8) with length `n`. |
|
for n in 0..length { |
|
this.set_field(n, Value::undefined()); |
|
} |
|
} |
|
1 if args[0].is_double() => { |
|
return ctx.throw_range_error("invalid array length"); |
|
} |
|
_ => { |
|
for (n, value) in args.iter().enumerate() { |
|
this.set_field(n, value.clone()); |
|
} |
|
} |
|
} |
|
|
|
// finally create length property |
|
let length = Property::data_descriptor( |
|
length.into(), |
|
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, |
|
); |
|
|
|
this.set_property("length".to_string(), length); |
|
|
|
Ok(this.clone()) |
|
} |
|
|
|
/// `Array.isArray( arg )` |
|
/// |
|
/// The isArray function takes one argument arg, and returns the Boolean value true |
|
/// if the argument is an object whose class internal property is "Array"; otherwise it returns false. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.isarray |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray |
|
pub(crate) fn is_array( |
|
_this: &Value, |
|
args: &[Value], |
|
_interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
match args.get(0).and_then(|x| x.as_object()) { |
|
Some(object) => Ok(Value::from(object.is_array())), |
|
None => Ok(Value::from(false)), |
|
} |
|
} |
|
|
|
/// `Array.prototype.concat(...arguments)` |
|
/// |
|
/// When the concat method is called with zero or more arguments, it returns an |
|
/// array containing the array elements of the object followed by the array |
|
/// elements of each argument in order. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.concat |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat |
|
pub(crate) fn concat(this: &Value, args: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
if args.is_empty() { |
|
// If concat is called with no arguments, it returns the original array |
|
return Ok(this.clone()); |
|
} |
|
|
|
// Make a new array (using this object as the prototype basis for the new |
|
// one) |
|
let mut new_values: Vec<Value> = Vec::new(); |
|
|
|
let this_length = this.get_field("length").as_number().unwrap() as i32; |
|
for n in 0..this_length { |
|
new_values.push(this.get_field(n)); |
|
} |
|
|
|
for concat_array in args { |
|
let concat_length = concat_array.get_field("length").as_number().unwrap() as i32; |
|
for n in 0..concat_length { |
|
new_values.push(concat_array.get_field(n)); |
|
} |
|
} |
|
|
|
Self::construct_array(this, &new_values) |
|
} |
|
|
|
/// `Array.prototype.push( ...items )` |
|
/// |
|
/// The arguments are appended to the end of the array, in the order in which |
|
/// they appear. The new length of the array is returned as the result of the |
|
/// call. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.push |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push |
|
pub(crate) fn push(this: &Value, args: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
let new_array = Self::add_to_array_object(this, args)?; |
|
Ok(new_array.get_field("length")) |
|
} |
|
|
|
/// `Array.prototype.pop()` |
|
/// |
|
/// The last element of the array is removed from the array and returned. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.pop |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop |
|
pub(crate) fn pop(this: &Value, _: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
let curr_length = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
if curr_length < 1 { |
|
return Ok(Value::undefined()); |
|
} |
|
let pop_index = curr_length.wrapping_sub(1); |
|
let pop_value: Value = this.get_field(pop_index.to_string()); |
|
this.remove_property(pop_index); |
|
this.set_field("length", Value::from(pop_index)); |
|
Ok(pop_value) |
|
} |
|
|
|
/// `Array.prototype.forEach( callbackFn [ , thisArg ] )` |
|
/// |
|
/// This method executes the provided callback function for each element in the array. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.foreach |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach |
|
pub(crate) fn for_each( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from("Missing argument for Array.prototype.forEach")); |
|
} |
|
|
|
let callback_arg = args.get(0).expect("Could not get `callbackFn` argument."); |
|
let this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
|
|
let length = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
for i in 0..length { |
|
let element = this.get_field(i); |
|
let arguments = [element, Value::from(i), this.clone()]; |
|
|
|
interpreter.call(callback_arg, &this_arg, &arguments)?; |
|
} |
|
|
|
Ok(Value::undefined()) |
|
} |
|
|
|
/// `Array.prototype.join( separator )` |
|
/// |
|
/// The elements of the array are converted to Strings, and these Strings are |
|
/// then concatenated, separated by occurrences of the separator. If no |
|
/// separator is provided, a single comma is used as the separator. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.join |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join |
|
pub(crate) fn join(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result<Value> { |
|
let separator = if args.is_empty() { |
|
String::from(",") |
|
} else { |
|
args.get(0) |
|
.expect("Could not get argument") |
|
.to_string(ctx)? |
|
.to_string() |
|
}; |
|
|
|
let mut elem_strs = Vec::new(); |
|
let length = this.get_field("length").as_number().unwrap() as i32; |
|
for n in 0..length { |
|
let elem_str = this.get_field(n).to_string(ctx)?.to_string(); |
|
elem_strs.push(elem_str); |
|
} |
|
|
|
Ok(Value::from(elem_strs.join(&separator))) |
|
} |
|
|
|
/// `Array.prototype.toString( separator )` |
|
/// |
|
/// The toString function is intentionally generic; it does not require that |
|
/// its this value be an Array object. Therefore it can be transferred to |
|
/// other kinds of objects for use as a method. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.tostring |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString |
|
#[allow(clippy::wrong_self_convention)] |
|
pub(crate) fn to_string(this: &Value, _args: &[Value], ctx: &mut Interpreter) -> Result<Value> { |
|
let method_name = "join"; |
|
let mut arguments = vec![Value::from(",")]; |
|
// 2. |
|
let mut method = this.get_field(method_name); |
|
// 3. |
|
if !method.is_function() { |
|
method = ctx |
|
.realm |
|
.global_obj |
|
.get_field("Object") |
|
.get_field(PROTOTYPE) |
|
.get_field("toString"); |
|
|
|
arguments = Vec::new(); |
|
} |
|
// 4. |
|
let join = ctx.call(&method, this, &arguments)?; |
|
|
|
let string = if let Value::String(ref s) = join { |
|
Value::from(s.as_str()) |
|
} else { |
|
Value::from("") |
|
}; |
|
|
|
Ok(string) |
|
} |
|
|
|
/// `Array.prototype.reverse()` |
|
/// |
|
/// The elements of the array are rearranged so as to reverse their order. |
|
/// The object is returned as the result of the call. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reverse |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse |
|
#[allow(clippy::else_if_without_else)] |
|
pub(crate) fn reverse(this: &Value, _: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
let len = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let middle: i32 = len.wrapping_div(2); |
|
|
|
for lower in 0..middle { |
|
let upper = len.wrapping_sub(lower).wrapping_sub(1); |
|
|
|
let upper_exists = this.has_field(upper); |
|
let lower_exists = this.has_field(lower); |
|
|
|
let upper_value = this.get_field(upper); |
|
let lower_value = this.get_field(lower); |
|
|
|
if upper_exists && lower_exists { |
|
this.set_field(upper, lower_value); |
|
this.set_field(lower, upper_value); |
|
} else if upper_exists { |
|
this.set_field(lower, upper_value); |
|
this.remove_property(upper); |
|
} else if lower_exists { |
|
this.set_field(upper, lower_value); |
|
this.remove_property(lower); |
|
} |
|
} |
|
|
|
Ok(this.clone()) |
|
} |
|
|
|
/// `Array.prototype.shift()` |
|
/// |
|
/// The first element of the array is removed from the array and returned. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.shift |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift |
|
pub(crate) fn shift(this: &Value, _: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
let len = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
if len == 0 { |
|
this.set_field("length", 0); |
|
return Ok(Value::undefined()); |
|
} |
|
|
|
let first: Value = this.get_field(0); |
|
|
|
for k in 1..len { |
|
let from = k; |
|
let to = k.wrapping_sub(1); |
|
|
|
let from_value = this.get_field(from); |
|
if from_value.is_undefined() { |
|
this.remove_property(to); |
|
} else { |
|
this.set_field(to, from_value); |
|
} |
|
} |
|
|
|
let final_index = len.wrapping_sub(1); |
|
this.remove_property(final_index); |
|
this.set_field("length", Value::from(final_index)); |
|
|
|
Ok(first) |
|
} |
|
|
|
/// `Array.prototype.unshift( ...items )` |
|
/// |
|
/// The arguments are prepended to the start of the array, such that their order |
|
/// within the array is the same as the order in which they appear in the |
|
/// argument list. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.unshift |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift |
|
pub(crate) fn unshift(this: &Value, args: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
let len = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let arg_c: i32 = args.len() as i32; |
|
|
|
if arg_c > 0 { |
|
for k in (1..=len).rev() { |
|
let from = k.wrapping_sub(1); |
|
let to = k.wrapping_add(arg_c).wrapping_sub(1); |
|
|
|
let from_value = this.get_field(from); |
|
if from_value.is_undefined() { |
|
this.remove_property(to); |
|
} else { |
|
this.set_field(to, from_value); |
|
} |
|
} |
|
for j in 0..arg_c { |
|
this.set_field( |
|
j, |
|
args.get(j as usize) |
|
.expect("Could not get argument") |
|
.clone(), |
|
); |
|
} |
|
} |
|
|
|
let temp = len.wrapping_add(arg_c); |
|
this.set_field("length", Value::from(temp)); |
|
Ok(Value::from(temp)) |
|
} |
|
|
|
/// `Array.prototype.every( callback, [ thisArg ] )` |
|
/// |
|
/// The every method executes the provided callback function once for each |
|
/// element present in the array until it finds the one where callback returns |
|
/// a falsy value. It returns `false` if it finds such element, otherwise it |
|
/// returns `true`. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.every |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every |
|
pub(crate) fn every( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from( |
|
"missing callback when calling function Array.prototype.every", |
|
)); |
|
} |
|
let callback = &args[0]; |
|
let this_arg = if args.len() > 1 { |
|
args[1].clone() |
|
} else { |
|
Value::undefined() |
|
}; |
|
let mut i = 0; |
|
let max_len = this.get_field("length").as_number().unwrap() as i32; |
|
let mut len = max_len; |
|
while i < len { |
|
let element = this.get_field(i); |
|
let arguments = [element, Value::from(i), this.clone()]; |
|
let result = interpreter.call(callback, &this_arg, &arguments)?; |
|
if !result.to_boolean() { |
|
return Ok(Value::from(false)); |
|
} |
|
len = min( |
|
max_len, |
|
this.get_field("length").as_number().unwrap() as i32, |
|
); |
|
i += 1; |
|
} |
|
Ok(Value::from(true)) |
|
} |
|
|
|
/// `Array.prototype.map( callback, [ thisArg ] )` |
|
/// |
|
/// For each element in the array the callback function is called, and a new |
|
/// array is constructed from the return values of these calls. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.map |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map |
|
pub(crate) fn map( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from( |
|
"missing argument 0 when calling function Array.prototype.map", |
|
)); |
|
} |
|
|
|
let callback = args.get(0).cloned().unwrap_or_else(Value::undefined); |
|
let this_val = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
|
|
let length = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let new = Self::new_array(interpreter)?; |
|
|
|
let values: Vec<Value> = (0..length) |
|
.map(|idx| { |
|
let element = this.get_field(idx); |
|
let args = [element, Value::from(idx), new.clone()]; |
|
|
|
interpreter |
|
.call(&callback, &this_val, &args) |
|
.unwrap_or_else(|_| Value::undefined()) |
|
}) |
|
.collect(); |
|
|
|
Self::construct_array(&new, &values) |
|
} |
|
|
|
/// `Array.prototype.indexOf( searchElement[, fromIndex ] )` |
|
/// |
|
/// |
|
/// indexOf compares searchElement to the elements of the array, in ascending order, |
|
/// using the Strict Equality Comparison algorithm, and if found at one or more indices, |
|
/// returns the smallest such index; otherwise, -1 is returned. |
|
/// |
|
/// The optional second argument fromIndex defaults to 0 (i.e. the whole array is searched). |
|
/// If it is greater than or equal to the length of the array, -1 is returned, |
|
/// i.e. the array will not be searched. If it is negative, it is used as the offset |
|
/// from the end of the array to compute fromIndex. If the computed index is less than 0, |
|
/// the whole array will be searched. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.indexof |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf |
|
pub(crate) fn index_of(this: &Value, args: &[Value], _: &mut Interpreter) -> Result<Value> { |
|
// If no arguments, return -1. Not described in spec, but is what chrome does. |
|
if args.is_empty() { |
|
return Ok(Value::from(-1)); |
|
} |
|
|
|
let search_element = args[0].clone(); |
|
let len = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let mut idx = match args.get(1) { |
|
Some(from_idx_ptr) => { |
|
let from_idx = from_idx_ptr.as_number().unwrap() as i32; |
|
|
|
if from_idx < 0 { |
|
len + from_idx |
|
} else { |
|
from_idx |
|
} |
|
} |
|
None => 0, |
|
}; |
|
|
|
while idx < len { |
|
let check_element = this.get_field(idx).clone(); |
|
|
|
if check_element.strict_equals(&search_element) { |
|
return Ok(Value::from(idx)); |
|
} |
|
|
|
idx += 1; |
|
} |
|
|
|
Ok(Value::from(-1)) |
|
} |
|
|
|
/// `Array.prototype.lastIndexOf( searchElement[, fromIndex ] )` |
|
/// |
|
/// |
|
/// lastIndexOf compares searchElement to the elements of the array in descending order |
|
/// using the Strict Equality Comparison algorithm, and if found at one or more indices, |
|
/// returns the largest such index; otherwise, -1 is returned. |
|
/// |
|
/// The optional second argument fromIndex defaults to the array's length minus one |
|
/// (i.e. the whole array is searched). If it is greater than or equal to the length of the array, |
|
/// the whole array will be searched. If it is negative, it is used as the offset from the end |
|
/// of the array to compute fromIndex. If the computed index is less than 0, -1 is returned. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.lastindexof |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf |
|
pub(crate) fn last_index_of( |
|
this: &Value, |
|
args: &[Value], |
|
_: &mut Interpreter, |
|
) -> Result<Value> { |
|
// If no arguments, return -1. Not described in spec, but is what chrome does. |
|
if args.is_empty() { |
|
return Ok(Value::from(-1)); |
|
} |
|
|
|
let search_element = args[0].clone(); |
|
let len = this |
|
.get_field("length") |
|
.as_number() |
|
.expect("length was not a number") as i32; |
|
|
|
let mut idx = match args.get(1) { |
|
Some(from_idx_ptr) => { |
|
let from_idx = from_idx_ptr.as_number().unwrap() as i32; |
|
|
|
if from_idx >= 0 { |
|
min(from_idx, len - 1) |
|
} else { |
|
len + from_idx |
|
} |
|
} |
|
None => len - 1, |
|
}; |
|
|
|
while idx >= 0 { |
|
let check_element = this.get_field(idx).clone(); |
|
|
|
if check_element.strict_equals(&search_element) { |
|
return Ok(Value::from(idx)); |
|
} |
|
|
|
idx -= 1; |
|
} |
|
|
|
Ok(Value::from(-1)) |
|
} |
|
|
|
/// `Array.prototype.find( callback, [thisArg] )` |
|
/// |
|
/// The find method executes the callback function once for each index of the array |
|
/// until the callback returns a truthy value. If so, find immediately returns the value |
|
/// of that element. Otherwise, find returns undefined. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.find |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find |
|
pub(crate) fn find( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from( |
|
"missing callback when calling function Array.prototype.find", |
|
)); |
|
} |
|
let callback = &args[0]; |
|
let this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
let len = this.get_field("length").as_number().unwrap() as i32; |
|
for i in 0..len { |
|
let element = this.get_field(i); |
|
let arguments = [element.clone(), Value::from(i), this.clone()]; |
|
let result = interpreter.call(callback, &this_arg, &arguments)?; |
|
if result.to_boolean() { |
|
return Ok(element); |
|
} |
|
} |
|
Ok(Value::undefined()) |
|
} |
|
|
|
/// `Array.prototype.findIndex( predicate [ , thisArg ] )` |
|
/// |
|
/// This method executes the provided predicate function for each element of the array. |
|
/// If the predicate function returns `true` for an element, this method returns the index of the element. |
|
/// If all elements return `false`, the value `-1` is returned. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.findindex |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex |
|
pub(crate) fn find_index( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from( |
|
"Missing argument for Array.prototype.findIndex", |
|
)); |
|
} |
|
|
|
let predicate_arg = args.get(0).expect("Could not get `predicate` argument."); |
|
|
|
let this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
|
|
let length = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
for i in 0..length { |
|
let element = this.get_field(i); |
|
let arguments = [element, Value::from(i), this.clone()]; |
|
|
|
let result = interpreter.call(predicate_arg, &this_arg, &arguments)?; |
|
|
|
if result.to_boolean() { |
|
return Ok(Value::rational(f64::from(i))); |
|
} |
|
} |
|
|
|
Ok(Value::rational(-1_f64)) |
|
} |
|
|
|
/// `Array.prototype.fill( value[, start[, end]] )` |
|
/// |
|
/// The method fills (modifies) all the elements of an array from start index (default 0) |
|
/// to an end index (default array length) with a static value. It returns the modified array. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.fill |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill |
|
pub(crate) fn fill(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result<Value> { |
|
let len: i32 = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let default_value = Value::undefined(); |
|
let value = args.get(0).unwrap_or(&default_value); |
|
let relative_start = args.get(1).unwrap_or(&default_value).to_number(ctx)? as i32; |
|
let relative_end_val = args.get(2).unwrap_or(&default_value); |
|
let relative_end = if relative_end_val.is_undefined() { |
|
len |
|
} else { |
|
relative_end_val.to_number(ctx)? as i32 |
|
}; |
|
let start = if relative_start < 0 { |
|
max(len + relative_start, 0) |
|
} else { |
|
min(relative_start, len) |
|
}; |
|
let fin = if relative_end < 0 { |
|
max(len + relative_end, 0) |
|
} else { |
|
min(relative_end, len) |
|
}; |
|
|
|
for i in start..fin { |
|
this.set_field(i, value.clone()); |
|
} |
|
|
|
Ok(this.clone()) |
|
} |
|
|
|
/// `Array.prototype.includes( valueToFind [, fromIndex] )` |
|
/// |
|
/// Determines whether an array includes a certain value among its entries, returning `true` or `false` as appropriate. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.includes |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes |
|
pub(crate) fn includes_value( |
|
this: &Value, |
|
args: &[Value], |
|
_: &mut Interpreter, |
|
) -> Result<Value> { |
|
let search_element = args.get(0).cloned().unwrap_or_else(Value::undefined); |
|
|
|
let length = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
for idx in 0..length { |
|
let check_element = this.get_field(idx).clone(); |
|
|
|
if same_value_zero(&check_element, &search_element) { |
|
return Ok(Value::from(true)); |
|
} |
|
} |
|
|
|
Ok(Value::from(false)) |
|
} |
|
|
|
/// `Array.prototype.slice( [begin[, end]] )` |
|
/// |
|
/// The slice method takes two arguments, start and end, and returns an array containing the |
|
/// elements of the array from element start up to, but not including, element end (or through the |
|
/// end of the array if end is undefined). If start is negative, it is treated as length + start |
|
/// where length is the length of the array. If end is negative, it is treated as length + end where |
|
/// length is the length of the array. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.slice |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice |
|
pub(crate) fn slice( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
let new_array = Self::new_array(interpreter)?; |
|
let len = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let start = match args.get(0) { |
|
Some(v) => v.as_number().unwrap() as i32, |
|
None => 0, |
|
}; |
|
let end = match args.get(1) { |
|
Some(v) => v.as_number().unwrap() as i32, |
|
None => len, |
|
}; |
|
|
|
let from = if start < 0 { |
|
max(len.wrapping_add(start), 0) |
|
} else { |
|
min(start, len) |
|
}; |
|
let to = if end < 0 { |
|
max(len.wrapping_add(end), 0) |
|
} else { |
|
min(end, len) |
|
}; |
|
|
|
let span = max(to.wrapping_sub(from), 0); |
|
let mut new_array_len: i32 = 0; |
|
for i in from..from.wrapping_add(span) { |
|
new_array.set_field(new_array_len, this.get_field(i)); |
|
new_array_len = new_array_len.wrapping_add(1); |
|
} |
|
new_array.set_field("length", Value::from(new_array_len)); |
|
Ok(new_array) |
|
} |
|
|
|
/// `Array.prototype.filter( callback, [ thisArg ] )` |
|
/// |
|
/// For each element in the array the callback function is called, and a new |
|
/// array is constructed for every value whose callback returned a truthy value. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.filter |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter |
|
pub(crate) fn filter( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from( |
|
"missing argument 0 when calling function Array.prototype.filter", |
|
)); |
|
} |
|
|
|
let callback = args.get(0).cloned().unwrap_or_else(Value::undefined); |
|
let this_val = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
|
|
let length = this.get_field("length").as_number().unwrap() as i32; |
|
|
|
let new = Self::new_array(interpreter)?; |
|
|
|
let values = (0..length) |
|
.filter_map(|idx| { |
|
let element = this.get_field(idx); |
|
|
|
let args = [element.clone(), Value::from(idx), new.clone()]; |
|
|
|
let callback_result = interpreter |
|
.call(&callback, &this_val, &args) |
|
.unwrap_or_else(|_| Value::undefined()); |
|
|
|
if callback_result.to_boolean() { |
|
Some(element) |
|
} else { |
|
None |
|
} |
|
}) |
|
.collect::<Vec<Value>>(); |
|
|
|
Self::construct_array(&new, &values) |
|
} |
|
|
|
/// Array.prototype.some ( callbackfn [ , thisArg ] ) |
|
/// |
|
/// The some method tests whether at least one element in the array passes |
|
/// the test implemented by the provided callback function. It returns a Boolean value, |
|
/// true if the callback function returns a truthy value for at least one element |
|
/// in the array. Otherwise, false. |
|
/// |
|
/// Caution: Calling this method on an empty array returns false for any condition! |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.some |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some |
|
pub(crate) fn some( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
if args.is_empty() { |
|
return Err(Value::from( |
|
"missing callback when calling function Array.prototype.some", |
|
)); |
|
} |
|
let callback = &args[0]; |
|
let this_arg = if args.len() > 1 { |
|
args[1].clone() |
|
} else { |
|
Value::undefined() |
|
}; |
|
let mut i = 0; |
|
let max_len = this.get_field("length").as_number().unwrap() as i32; |
|
let mut len = max_len; |
|
while i < len { |
|
let element = this.get_field(i); |
|
let arguments = [element, Value::from(i), this.clone()]; |
|
let result = interpreter.call(callback, &this_arg, &arguments)?; |
|
if result.to_boolean() { |
|
return Ok(Value::from(true)); |
|
} |
|
// the length of the array must be updated because the callback can mutate it. |
|
len = min( |
|
max_len, |
|
this.get_field("length").as_number().unwrap() as i32, |
|
); |
|
i += 1; |
|
} |
|
Ok(Value::from(false)) |
|
} |
|
|
|
/// `Array.prototype.reduce( callbackFn [ , initialValue ] )` |
|
/// |
|
/// The reduce method traverses left to right starting from the first defined value in the array, |
|
/// accumulating a value using a given callback function. It returns the accumulated value. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduce |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce |
|
pub(crate) fn reduce( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
let this = this.to_object(interpreter)?; |
|
let callback = match args.get(0) { |
|
Some(value) if value.is_function() => value, |
|
_ => return interpreter.throw_type_error("Reduce was called without a callback"), |
|
}; |
|
let initial_value = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
let mut length = this.get_field("length").to_length(interpreter)?; |
|
if length == 0 && initial_value.is_undefined() { |
|
return interpreter |
|
.throw_type_error("Reduce was called on an empty array and with no initial value"); |
|
} |
|
let mut k = 0; |
|
let mut accumulator = if initial_value.is_undefined() { |
|
let mut k_present = false; |
|
while k < length { |
|
if this.has_field(k) { |
|
k_present = true; |
|
break; |
|
} |
|
k += 1; |
|
} |
|
if !k_present { |
|
return interpreter.throw_type_error( |
|
"Reduce was called on an empty array and with no initial value", |
|
); |
|
} |
|
let result = this.get_field(k); |
|
k += 1; |
|
result |
|
} else { |
|
initial_value |
|
}; |
|
while k < length { |
|
if this.has_field(k) { |
|
let arguments = [accumulator, this.get_field(k), Value::from(k), this.clone()]; |
|
accumulator = interpreter.call(&callback, &Value::undefined(), &arguments)?; |
|
/* We keep track of possibly shortened length in order to prevent unnecessary iteration. |
|
It may also be necessary to do this since shortening the array length does not |
|
delete array elements. See: https://github.com/boa-dev/boa/issues/557 */ |
|
length = min(length, this.get_field("length").to_length(interpreter)?); |
|
} |
|
k += 1; |
|
} |
|
Ok(accumulator) |
|
} |
|
|
|
/// `Array.prototype.reduceRight( callbackFn [ , initialValue ] )` |
|
/// |
|
/// The reduceRight method traverses right to left starting from the last defined value in the array, |
|
/// accumulating a value using a given callback function. It returns the accumulated value. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// - [MDN documentation][mdn] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduceright |
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight |
|
pub(crate) fn reduce_right( |
|
this: &Value, |
|
args: &[Value], |
|
interpreter: &mut Interpreter, |
|
) -> Result<Value> { |
|
let this = this.to_object(interpreter)?; |
|
let callback = match args.get(0) { |
|
Some(value) if value.is_function() => value, |
|
_ => return interpreter.throw_type_error("reduceRight was called without a callback"), |
|
}; |
|
let initial_value = args.get(1).cloned().unwrap_or_else(Value::undefined); |
|
let mut length = this.get_field("length").to_length(interpreter)?; |
|
if length == 0 { |
|
if initial_value.is_undefined() { |
|
return interpreter.throw_type_error( |
|
"reduceRight was called on an empty array and with no initial value", |
|
); |
|
} else { |
|
// early return to prevent usize subtraction errors |
|
return Ok(initial_value); |
|
} |
|
} |
|
let mut k = length - 1; |
|
let mut accumulator = if initial_value.is_undefined() { |
|
let mut k_present = false; |
|
loop { |
|
if this.has_field(k) { |
|
k_present = true; |
|
break; |
|
} |
|
// check must be done at the end to prevent usize subtraction error |
|
if k == 0 { |
|
break; |
|
} |
|
k -= 1; |
|
} |
|
if !k_present { |
|
return interpreter.throw_type_error( |
|
"reduceRight was called on an empty array and with no initial value", |
|
); |
|
} |
|
let result = this.get_field(k); |
|
k -= 1; |
|
result |
|
} else { |
|
initial_value |
|
}; |
|
loop { |
|
if this.has_field(k) { |
|
let arguments = [accumulator, this.get_field(k), Value::from(k), this.clone()]; |
|
accumulator = interpreter.call(&callback, &Value::undefined(), &arguments)?; |
|
/* We keep track of possibly shortened length in order to prevent unnecessary iteration. |
|
It may also be necessary to do this since shortening the array length does not |
|
delete array elements. See: https://github.com/boa-dev/boa/issues/557 */ |
|
length = min(length, this.get_field("length").to_length(interpreter)?); |
|
|
|
// move k to the last defined element if necessary or return if the length was set to 0 |
|
if k >= length { |
|
if length == 0 { |
|
return Ok(accumulator); |
|
} else { |
|
k = length - 1; |
|
continue; |
|
} |
|
} |
|
} |
|
if k == 0 { |
|
break; |
|
} |
|
k -= 1; |
|
} |
|
Ok(accumulator) |
|
} |
|
|
|
/// Initialise the `Array` object on the global object. |
|
#[inline] |
|
pub(crate) fn init(interpreter: &mut Interpreter) -> (&'static str, Value) { |
|
let global = interpreter.global(); |
|
let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); |
|
|
|
// Create prototype |
|
let prototype = Value::new_object(Some(global)); |
|
let length = Property::default().value(Value::from(0)); |
|
|
|
prototype.set_property("length", length); |
|
|
|
make_builtin_fn(Self::concat, "concat", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::push, "push", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::index_of, "indexOf", &prototype, 1, interpreter); |
|
make_builtin_fn( |
|
Self::last_index_of, |
|
"lastIndexOf", |
|
&prototype, |
|
1, |
|
interpreter, |
|
); |
|
make_builtin_fn(Self::includes_value, "includes", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::map, "map", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::fill, "fill", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::for_each, "forEach", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::filter, "filter", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::pop, "pop", &prototype, 0, interpreter); |
|
make_builtin_fn(Self::join, "join", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::to_string, "toString", &prototype, 0, interpreter); |
|
make_builtin_fn(Self::reverse, "reverse", &prototype, 0, interpreter); |
|
make_builtin_fn(Self::shift, "shift", &prototype, 0, interpreter); |
|
make_builtin_fn(Self::unshift, "unshift", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::every, "every", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::find, "find", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::find_index, "findIndex", &prototype, 1, interpreter); |
|
make_builtin_fn(Self::slice, "slice", &prototype, 2, interpreter); |
|
make_builtin_fn(Self::some, "some", &prototype, 2, interpreter); |
|
make_builtin_fn(Self::reduce, "reduce", &prototype, 2, interpreter); |
|
make_builtin_fn( |
|
Self::reduce_right, |
|
"reduceRight", |
|
&prototype, |
|
2, |
|
interpreter, |
|
); |
|
|
|
let array = make_constructor_fn( |
|
Self::NAME, |
|
Self::LENGTH, |
|
Self::make_array, |
|
global, |
|
prototype, |
|
true, |
|
true, |
|
); |
|
|
|
// Static Methods |
|
make_builtin_fn(Self::is_array, "isArray", &array, 1, interpreter); |
|
|
|
(Self::NAME, array) |
|
} |
|
}
|
|
|