From 9f6aa1972ce37f10ac15d71a46a616b0652a2786 Mon Sep 17 00:00:00 2001 From: Halid Odat Date: Mon, 21 Feb 2022 12:04:53 +0000 Subject: [PATCH] Feature `JsArray` (#1746) This PR introduces a new API for JavaScript builtin objects in Rust (such as `Array`, `Map`, `Proxy`, etc). Rather than just expose the raw builtin functions as discussed here #1692 (though having raw API exposed may be nice as well), In this PR we introduce a very light wrapper around the raw API, for a more pleasant user experience. The wrapper implements functions that are specific to the wrapper type (for `Array` this would be methods like `pop`, `push`, etc) as well as implementing `Deref` so we can call `JsObject` functions without converting to `JsObject` and `Into` for easy to `JsValue` conversions. Please check `jsarray.rs` in the `examples` --- boa/examples/jsarray.rs | 107 ++++++++++ boa/src/object/jsarray.rs | 364 +++++++++++++++++++++++++++++++++++ boa/src/object/mod.rs | 8 + boa/src/value/conversions.rs | 2 +- 4 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 boa/examples/jsarray.rs create mode 100644 boa/src/object/jsarray.rs diff --git a/boa/examples/jsarray.rs b/boa/examples/jsarray.rs new file mode 100644 index 0000000000..4567af97d2 --- /dev/null +++ b/boa/examples/jsarray.rs @@ -0,0 +1,107 @@ +use boa::{ + object::{FunctionBuilder, JsArray}, + Context, JsValue, +}; + +fn main() -> Result<(), JsValue> { + // We create a new `Context` to create a new Javascript executor. + let context = &mut Context::default(); + + // Create an empty array. + let array = JsArray::new(context); + + assert!(array.is_empty(context)?); + + array.push("Hello, world", context)?; // [ "Hello, world" ] + array.push(true, context)?; // [ "Hello, world", true ] + + assert!(!array.is_empty(context)?); + + assert_eq!(array.pop(context)?, JsValue::new(true)); // [ "Hello, world" ] + assert_eq!(array.pop(context)?, JsValue::new("Hello, world")); // [ ] + assert_eq!(array.pop(context)?, JsValue::undefined()); // [ ] + + array.push(1, context)?; // [ 1 ] + + assert_eq!(array.pop(context)?, JsValue::new(1)); // [ ] + assert_eq!(array.pop(context)?, JsValue::undefined()); // [ ] + + array.push_items( + &[ + JsValue::new(10), + JsValue::new(11), + JsValue::new(12), + JsValue::new(13), + JsValue::new(14), + ], + context, + )?; // [ 10, 11, 12, 13, 14 ] + + array.reverse(context)?; // [ 14, 13, 12, 11, 10 ] + + assert_eq!(array.index_of(12, None, context)?, Some(2)); + + // We can also use JsObject method `.get()` through the Deref trait. + let element = array.get(2, context)?; // array[ 0 ] + assert_eq!(element, JsValue::new(12)); + // Or we can use the `.at(index)` method. + assert_eq!(array.at(0, context)?, JsValue::new(14)); // first element + assert_eq!(array.at(-1, context)?, JsValue::new(10)); // last element + + // Join the array with an optional separator (default ","). + let joined_array = array.join(None, context)?; + assert_eq!(joined_array, "14,13,12,11,10"); + + array.fill(false, Some(1), Some(4), context)?; + + let joined_array = array.join(Some("::".into()), context)?; + assert_eq!(joined_array, "14::false::false::false::10"); + + let filter_callback = FunctionBuilder::native(context, |_this, args, _context| { + Ok(args.get(0).cloned().unwrap_or_default().is_number().into()) + }) + .build(); + + let map_callback = FunctionBuilder::native(context, |_this, args, context| { + args.get(0) + .cloned() + .unwrap_or_default() + .pow(&JsValue::new(2), context) + }) + .build(); + + let mut data = Vec::new(); + for i in 1..=5 { + data.push(JsValue::new(i)); + } + let another_array = JsArray::from_iter(data, context); // [ 1, 2, 3, 4, 5] + + let chained_array = array // [ 14, false, false, false, 10 ] + .filter(filter_callback, None, context)? // [ 14, 10 ] + .map(map_callback, None, context)? // [ 196, 100 ] + .sort(None, context)? // [ 100, 196 ] + .concat(&[another_array.into()], context)? // [ 100, 196, 1, 2, 3, 4, 5 ] + .slice(Some(1), Some(5), context)?; // [ 196, 1, 2, 3 ] + + assert_eq!(chained_array.join(None, context)?, "196,1,2,3"); + + let reduce_callback = FunctionBuilder::native(context, |_this, args, context| { + let accumulator = args.get(0).cloned().unwrap_or_default(); + let value = args.get(1).cloned().unwrap_or_default(); + + accumulator.add(&value, context) + }) + .build(); + + assert_eq!( + chained_array.reduce(reduce_callback, Some(JsValue::new(0)), context)?, + JsValue::new(202) + ); + + context + .global_object() + .clone() + .set("myArray", array, true, context)?; + + Ok(()) +} diff --git a/boa/src/object/jsarray.rs b/boa/src/object/jsarray.rs new file mode 100644 index 0000000000..41af6cdd0d --- /dev/null +++ b/boa/src/object/jsarray.rs @@ -0,0 +1,364 @@ +use std::ops::Deref; + +use crate::{ + builtins::Array, + gc::{Finalize, Trace}, + object::{JsObject, JsObjectType}, + Context, JsResult, JsString, JsValue, +}; + +/// JavaScript `Array` rust object. +#[derive(Debug, Clone, Trace, Finalize)] +pub struct JsArray { + inner: JsObject, +} + +impl JsArray { + /// Create a new empty array. + #[inline] + pub fn new(context: &mut Context) -> Self { + let inner = Array::array_create(0, None, context) + .expect("creating an empty array with the default prototype must not fail"); + + Self { inner } + } + + /// Create an array from a `IntoIterator` convertable object. + #[inline] + pub fn from_iter(elements: I, context: &mut Context) -> Self + where + I: IntoIterator, + { + Self { + inner: Array::create_array_from_list(elements, context), + } + } + + /// Create an array from a `JsObject`, if the object is not an array throw a `TypeError`. + /// + /// This does not copy the fields of the array, it only does a shallow copy. + #[inline] + pub fn from_object(object: JsObject, context: &mut Context) -> JsResult { + if object.borrow().is_array() { + Ok(Self { inner: object }) + } else { + context.throw_type_error("object is not an Array") + } + } + + /// Get the length of the array. + /// + /// Same a `array.length` in JavaScript. + #[inline] + pub fn length(&self, context: &mut Context) -> JsResult { + self.inner.length_of_array_like(context) + } + + /// Check if the array is empty, i.e. the `length` is zero. + #[inline] + pub fn is_empty(&self, context: &mut Context) -> JsResult { + self.inner.length_of_array_like(context).map(|len| len == 0) + } + + /// Push an element to the array. + #[inline] + pub fn push(&self, value: T, context: &mut Context) -> JsResult + where + T: Into, + { + self.push_items(&[value.into()], context) + } + + /// Pushes a slice of elements to the array. + #[inline] + pub fn push_items(&self, items: &[JsValue], context: &mut Context) -> JsResult { + Array::push(&self.inner.clone().into(), items, context) + } + + /// Pops an element from the array. + #[inline] + pub fn pop(&self, context: &mut Context) -> JsResult { + Array::pop(&self.inner.clone().into(), &[], context) + } + + #[inline] + pub fn at(&self, index: T, context: &mut Context) -> JsResult + where + T: Into, + { + Array::at(&self.inner.clone().into(), &[index.into().into()], context) + } + + #[inline] + pub fn shift(&self, context: &mut Context) -> JsResult { + Array::shift(&self.inner.clone().into(), &[], context) + } + + #[inline] + pub fn unshift(&self, items: &[JsValue], context: &mut Context) -> JsResult { + Array::shift(&self.inner.clone().into(), items, context) + } + + #[inline] + pub fn reverse(&self, context: &mut Context) -> JsResult { + Array::reverse(&self.inner.clone().into(), &[], context)?; + Ok(self.clone()) + } + + #[inline] + pub fn concat(&self, items: &[JsValue], context: &mut Context) -> JsResult { + let object = Array::concat(&self.inner.clone().into(), items, context)? + .as_object() + .cloned() + .expect("Array.prototype.filter should always return object"); + + Self::from_object(object, context) + } + + #[inline] + pub fn join(&self, separator: Option, context: &mut Context) -> JsResult { + Array::join(&self.inner.clone().into(), &[separator.into()], context).map(|x| { + x.as_string() + .cloned() + .expect("Array.prototype.join always returns string") + }) + } + + #[inline] + pub fn fill( + &self, + value: T, + start: Option, + end: Option, + context: &mut Context, + ) -> JsResult + where + T: Into, + { + Array::fill( + &self.inner.clone().into(), + &[value.into(), start.into(), end.into()], + context, + )?; + Ok(self.clone()) + } + + #[inline] + pub fn index_of( + &self, + search_element: T, + from_index: Option, + context: &mut Context, + ) -> JsResult> + where + T: Into, + { + let index = Array::index_of( + &self.inner.clone().into(), + &[search_element.into(), from_index.into()], + context, + )? + .as_number() + .expect("Array.prototype.indexOf should always return number"); + + #[allow(clippy::float_cmp)] + if index == -1.0 { + Ok(None) + } else { + Ok(Some(index as u32)) + } + } + + #[inline] + pub fn last_index_of( + &self, + search_element: T, + from_index: Option, + context: &mut Context, + ) -> JsResult> + where + T: Into, + { + let index = Array::last_index_of( + &self.inner.clone().into(), + &[search_element.into(), from_index.into()], + context, + )? + .as_number() + .expect("Array.prototype.lastIndexOf should always return number"); + + #[allow(clippy::float_cmp)] + if index == -1.0 { + Ok(None) + } else { + Ok(Some(index as u32)) + } + } + + #[inline] + pub fn find( + &self, + predicate: JsObject, + this_arg: Option, + context: &mut Context, + ) -> JsResult { + Array::find( + &self.inner.clone().into(), + &[predicate.into(), this_arg.into()], + context, + ) + } + + #[inline] + pub fn filter( + &self, + callback: JsObject, + this_arg: Option, + context: &mut Context, + ) -> JsResult { + let object = Array::filter( + &self.inner.clone().into(), + &[callback.into(), this_arg.into()], + context, + )? + .as_object() + .cloned() + .expect("Array.prototype.filter should always return object"); + + Self::from_object(object, context) + } + + #[inline] + pub fn map( + &self, + callback: JsObject, + this_arg: Option, + context: &mut Context, + ) -> JsResult { + let object = Array::map( + &self.inner.clone().into(), + &[callback.into(), this_arg.into()], + context, + )? + .as_object() + .cloned() + .expect("Array.prototype.map should always return object"); + + Self::from_object(object, context) + } + + #[inline] + pub fn every( + &self, + callback: JsObject, + this_arg: Option, + context: &mut Context, + ) -> JsResult { + let result = Array::every( + &self.inner.clone().into(), + &[callback.into(), this_arg.into()], + context, + )? + .as_boolean() + .expect("Array.prototype.every should always return boolean"); + + Ok(result) + } + + #[inline] + pub fn some( + &self, + callback: JsObject, + this_arg: Option, + context: &mut Context, + ) -> JsResult { + let result = Array::some( + &self.inner.clone().into(), + &[callback.into(), this_arg.into()], + context, + )? + .as_boolean() + .expect("Array.prototype.some should always return boolean"); + + Ok(result) + } + + #[inline] + pub fn sort(&self, compare_fn: Option, context: &mut Context) -> JsResult { + Array::sort(&self.inner.clone().into(), &[compare_fn.into()], context)?; + + Ok(self.clone()) + } + + #[inline] + pub fn slice( + &self, + start: Option, + end: Option, + context: &mut Context, + ) -> JsResult { + let object = Array::slice( + &self.inner.clone().into(), + &[start.into(), end.into()], + context, + )? + .as_object() + .cloned() + .expect("Array.prototype.slice should always return object"); + + Self::from_object(object, context) + } + + #[inline] + pub fn reduce( + &self, + callback: JsObject, + initial_value: Option, + context: &mut Context, + ) -> JsResult { + Array::reduce( + &self.inner.clone().into(), + &[callback.into(), initial_value.into()], + context, + ) + } + + #[inline] + pub fn reduce_right( + &self, + callback: JsObject, + initial_value: Option, + context: &mut Context, + ) -> JsResult { + Array::reduce_right( + &self.inner.clone().into(), + &[callback.into(), initial_value.into()], + context, + ) + } +} + +impl From for JsObject { + #[inline] + fn from(o: JsArray) -> Self { + o.inner.clone() + } +} + +impl From for JsValue { + #[inline] + fn from(o: JsArray) -> Self { + o.inner.clone().into() + } +} + +impl Deref for JsArray { + type Target = JsObject; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl JsObjectType for JsArray {} diff --git a/boa/src/object/mod.rs b/boa/src/object/mod.rs index f8faa2880b..4f1e429f49 100644 --- a/boa/src/object/mod.rs +++ b/boa/src/object/mod.rs @@ -53,10 +53,18 @@ use self::internal_methods::{ mod tests; pub(crate) mod internal_methods; +mod jsarray; mod jsobject; mod operations; mod property_map; +pub use jsarray::*; + +pub(crate) trait JsObjectType: + Into + Into + Deref +{ +} + /// Static `prototype`, usually set on constructors as a key to point to their respective prototype object. pub static PROTOTYPE: &str = "prototype"; diff --git a/boa/src/value/conversions.rs b/boa/src/value/conversions.rs index 667194642d..fb189060d2 100644 --- a/boa/src/value/conversions.rs +++ b/boa/src/value/conversions.rs @@ -153,7 +153,7 @@ where fn from(value: Option) -> Self { match value { Some(value) => value.into(), - None => Self::null(), + None => Self::undefined(), } } }