Rust编写的JavaScript引擎,该项目是一个试验性质的项目。
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

//! 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)
}
}