diff --git a/Cargo.lock b/Cargo.lock index 9498a77abd..7568ca5174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,6 +7,7 @@ dependencies = [ "bitflags", "criterion", "gc", + "indexmap", "jemallocator", "measureme", "num-bigint", @@ -145,9 +146,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.58" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" +checksum = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe" [[package]] name = "cfg-if" @@ -373,6 +374,15 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" +dependencies = [ + "autocfg", +] + [[package]] name = "itertools" version = "0.9.0" diff --git a/boa/Cargo.toml b/boa/Cargo.toml index 8d37d72f00..dad44d38e2 100644 --- a/boa/Cargo.toml +++ b/boa/Cargo.toml @@ -23,6 +23,7 @@ rustc-hash = "1.1.0" num-bigint = { version = "0.3.0", features = ["serde"] } num-integer = "0.1.43" bitflags = "1.2.1" +indexmap = "1.4.0" ryu-js = "0.2.0" # Optional Dependencies diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index bfe76015d3..3617fefc82 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -1064,6 +1064,7 @@ impl Array { global, prototype, true, + true, ); // Static Methods diff --git a/boa/src/builtins/bigint/mod.rs b/boa/src/builtins/bigint/mod.rs index d6352364c1..01f0ade220 100644 --- a/boa/src/builtins/bigint/mod.rs +++ b/boa/src/builtins/bigint/mod.rs @@ -215,6 +215,7 @@ impl BigInt { global, prototype, false, + true, ); make_builtin_fn(Self::as_int_n, "asIntN", &bigint_object, 2); diff --git a/boa/src/builtins/boolean/mod.rs b/boa/src/builtins/boolean/mod.rs index 81826821ef..6edad1e281 100644 --- a/boa/src/builtins/boolean/mod.rs +++ b/boa/src/builtins/boolean/mod.rs @@ -115,6 +115,7 @@ impl Boolean { global, prototype, true, + true, ); (Self::NAME, boolean_object) diff --git a/boa/src/builtins/error/mod.rs b/boa/src/builtins/error/mod.rs index 24943f00d0..828ab554ce 100644 --- a/boa/src/builtins/error/mod.rs +++ b/boa/src/builtins/error/mod.rs @@ -92,6 +92,7 @@ impl Error { global, prototype, true, + true, ); (Self::NAME, error_object) diff --git a/boa/src/builtins/error/range.rs b/boa/src/builtins/error/range.rs index 400122dd95..ba8e76574c 100644 --- a/boa/src/builtins/error/range.rs +++ b/boa/src/builtins/error/range.rs @@ -78,6 +78,7 @@ impl RangeError { global, prototype, true, + true, ); (Self::NAME, range_error_object) diff --git a/boa/src/builtins/error/reference.rs b/boa/src/builtins/error/reference.rs index 93d66488fc..842bebf1a8 100644 --- a/boa/src/builtins/error/reference.rs +++ b/boa/src/builtins/error/reference.rs @@ -76,6 +76,7 @@ impl ReferenceError { global, prototype, true, + true, ); (Self::NAME, reference_error_object) diff --git a/boa/src/builtins/error/syntax.rs b/boa/src/builtins/error/syntax.rs index 22a3da1f14..783551651a 100644 --- a/boa/src/builtins/error/syntax.rs +++ b/boa/src/builtins/error/syntax.rs @@ -80,6 +80,7 @@ impl SyntaxError { global, prototype, true, + true, ); (Self::NAME, syntax_error_object) diff --git a/boa/src/builtins/error/type.rs b/boa/src/builtins/error/type.rs index 727d836c50..27d3dbbf46 100644 --- a/boa/src/builtins/error/type.rs +++ b/boa/src/builtins/error/type.rs @@ -84,6 +84,7 @@ impl TypeError { global, prototype, true, + true, ); (Self::NAME, type_error_object) diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index 50f955f928..83e5de2965 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -24,6 +24,7 @@ use crate::{ syntax::ast::node::{FormalParameter, StatementList}, BoaProfiler, }; +use bitflags::bitflags; use gc::{unsafe_empty_trace, Finalize, Trace}; use std::fmt::{self, Debug}; @@ -90,6 +91,43 @@ unsafe impl Trace for FunctionBody { unsafe_empty_trace!(); } +bitflags! { + #[derive(Finalize, Default)] + struct FunctionFlags: u8 { + const CALLABLE = 0b0000_0001; + const CONSTRUCTABLE = 0b0000_0010; + } +} + +impl FunctionFlags { + fn from_parameters(callable: bool, constructable: bool) -> Self { + let mut flags = Self::default(); + + if callable { + flags |= Self::CALLABLE; + } + if constructable { + flags |= Self::CONSTRUCTABLE; + } + + flags + } + + #[inline] + fn is_callable(&self) -> bool { + self.contains(Self::CALLABLE) + } + + #[inline] + fn is_constructable(&self) -> bool { + self.contains(Self::CONSTRUCTABLE) + } +} + +unsafe impl Trace for FunctionFlags { + unsafe_empty_trace!(); +} + /// Boa representation of a Function Object. /// /// @@ -103,10 +141,8 @@ pub struct Function { pub this_mode: ThisMode, // Environment, built-in functions don't need Environments pub environment: Option, - /// Is it constructable - constructable: bool, - /// Is it callable. - callable: bool, + /// Is it constructable or + flags: FunctionFlags, } impl Function { @@ -126,8 +162,7 @@ impl Function { environment: scope, params: parameter_list.into(), this_mode, - constructable, - callable, + flags: FunctionFlags::from_parameters(callable, constructable), } } @@ -183,7 +218,7 @@ impl Function { interpreter: &mut Interpreter, ) -> ResultValue { let _timer = BoaProfiler::global().start_event("function::call", "function"); - if self.callable { + if self.flags.is_callable() { match self.body { FunctionBody::BuiltIn(func) => func(this, args_list, interpreter), FunctionBody::Ordinary(ref body) => { @@ -249,7 +284,7 @@ impl Function { args_list: &[Value], interpreter: &mut Interpreter, ) -> ResultValue { - if self.constructable { + if self.flags.is_constructable() { match self.body { FunctionBody::BuiltIn(func) => { func(this, args_list, interpreter)?; @@ -351,12 +386,12 @@ impl Function { /// Returns true if the function object is callable. pub fn is_callable(&self) -> bool { - self.callable + self.flags.is_callable() } /// Returns true if the function object is constructable. pub fn is_constructable(&self) -> bool { - self.constructable + self.flags.is_constructable() } } @@ -420,13 +455,14 @@ pub fn make_constructor_fn( global: &Value, prototype: Value, constructable: bool, + callable: bool, ) -> Value { let _timer = BoaProfiler::global().start_event(&format!("make_constructor_fn: {}", name), "init"); // Create the native function let mut function = Function::builtin(Vec::new(), body); - function.constructable = constructable; + function.flags = FunctionFlags::from_parameters(callable, constructable); let mut constructor = Object::function(function); @@ -507,7 +543,7 @@ pub fn init(global: &Value) -> (&str, Value) { let prototype = Value::new_object(Some(global)); let function_object = - make_constructor_fn("Function", 1, make_function, global, prototype, true); + make_constructor_fn("Function", 1, make_function, global, prototype, true, true); ("Function", function_object) } diff --git a/boa/src/builtins/map/mod.rs b/boa/src/builtins/map/mod.rs new file mode 100644 index 0000000000..3c04dae4c5 --- /dev/null +++ b/boa/src/builtins/map/mod.rs @@ -0,0 +1,311 @@ +#![allow(clippy::mutable_key_type)] + +use super::function::{make_builtin_fn, make_constructor_fn}; +use crate::{ + builtins::{ + object::{ObjectData, INSTANCE_PROTOTYPE, PROTOTYPE}, + property::Property, + value::{ResultValue, Value}, + }, + exec::Interpreter, + BoaProfiler, +}; +use ordered_map::OrderedMap; + +pub mod ordered_map; +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone)] +pub(crate) struct Map(OrderedMap); + +impl Map { + pub(crate) const NAME: &'static str = "Map"; + + pub(crate) const LENGTH: usize = 1; + + /// Helper function to set the size property. + fn set_size(this: &Value, size: usize) { + let size = Property::new() + .value(Value::from(size)) + .writable(false) + .configurable(false) + .enumerable(false); + + this.set_property("size".to_string(), size); + } + + /// `Map.prototype.set( key, value )` + /// + /// This method associates the value with the key. Returns the map object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.set + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set + pub(crate) fn set(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + let (key, value) = match args.len() { + 0 => (Value::Undefined, Value::Undefined), + 1 => (args[0].clone(), Value::Undefined), + _ => (args[0].clone(), args[1].clone()), + }; + + let size = if let Value::Object(ref object) = this { + let mut object = object.borrow_mut(); + if let Some(map) = object.as_map_mut() { + map.insert(key, value); + map.len() + } else { + return Err(ctx.construct_type_error("'this' is not a Map")); + } + } else { + return Err(ctx.construct_type_error("'this' is not a Map")); + }; + + Self::set_size(this, size); + Ok(this.clone()) + } + + /// `Map.prototype.delete( key )` + /// + /// This method removes the element associated with the key, if it exists. Returns true if there was an element, false otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.delete + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete + pub(crate) fn delete(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + let undefined = Value::Undefined; + let key = match args.len() { + 0 => &undefined, + _ => &args[0], + }; + + let (deleted, size) = if let Value::Object(ref object) = this { + let mut object = object.borrow_mut(); + if let Some(map) = object.as_map_mut() { + let deleted = map.remove(key).is_some(); + (deleted, map.len()) + } else { + return Err(ctx.construct_type_error("'this' is not a Map")); + } + } else { + return Err(ctx.construct_type_error("'this' is not a Map")); + }; + Self::set_size(this, size); + Ok(deleted.into()) + } + + /// `Map.prototype.get( key )` + /// + /// This method returns the value associated with the key, or undefined if there is none. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.get + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get + pub(crate) fn get(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + let undefined = Value::Undefined; + let key = match args.len() { + 0 => &undefined, + _ => &args[0], + }; + + if let Value::Object(ref object) = this { + let object = object.borrow(); + if let Some(map) = object.as_map_ref() { + return Ok(if let Some(result) = map.get(key) { + result.clone() + } else { + Value::Undefined + }); + } + } + + Err(ctx.construct_type_error("'this' is not a Map")) + } + + /// `Map.prototype.clear( )` + /// + /// This method removes all entries from the map. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.clear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear + pub(crate) fn clear(this: &Value, _: &[Value], _: &mut Interpreter) -> ResultValue { + this.set_data(ObjectData::Map(OrderedMap::new())); + + Self::set_size(this, 0); + + Ok(Value::Undefined) + } + + /// `Map.prototype.has( key )` + /// + /// This method checks if the map contains an entry with the given key. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has + pub(crate) fn has(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + let undefined = Value::Undefined; + let key = match args.len() { + 0 => &undefined, + _ => &args[0], + }; + + if let Value::Object(ref object) = this { + let object = object.borrow(); + if let Some(map) = object.as_map_ref() { + return Ok(map.contains_key(key).into()); + } + } + + Err(ctx.construct_type_error("'this' is not a Map")) + } + + /// `Map.prototype.forEach( callbackFn [ , thisArg ] )` + /// + /// This method executes the provided callback function for each key-value pair in the map. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.foreach + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach + pub(crate) fn for_each( + this: &Value, + args: &[Value], + interpreter: &mut Interpreter, + ) -> ResultValue { + if args.is_empty() { + return Err(Value::from("Missing argument for Map.prototype.forEach")); + } + + let callback_arg = &args[0]; + let this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); + + if let Value::Object(ref object) = this { + let object = object.borrow(); + if let Some(map) = object.as_map_ref().cloned() { + for (key, value) in map { + let arguments = [value, key, this.clone()]; + + interpreter.call(callback_arg, &this_arg, &arguments)?; + } + } + } + + Ok(Value::Undefined) + } + + /// Helper function to get a key-value pair from an array. + fn get_key_value(value: &Value) -> Option<(Value, Value)> { + if let Value::Object(object) = value { + if object.borrow().is_array() { + let (key, value) = match i32::from(&value.get_field("length")) { + 0 => (Value::Undefined, Value::Undefined), + 1 => (value.get_field("0"), Value::Undefined), + _ => (value.get_field("0"), value.get_field("1")), + }; + return Some((key, value)); + } + } + None + } + + /// Create a new map + pub(crate) fn make_map(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue { + // 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("Map").get_field(PROTOTYPE); + + this.set_internal_slot(INSTANCE_PROTOTYPE, prototype); + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + + // add our arguments in + let data = match args.len() { + 0 => OrderedMap::new(), + _ => match &args[0] { + Value::Object(object) => { + let object = object.borrow(); + if let Some(map) = object.as_map_ref().cloned() { + map + } else if object.is_array() { + let mut map = OrderedMap::new(); + let len = i32::from(&args[0].get_field("length")); + for i in 0..len { + let val = &args[0].get_field(i.to_string()); + let (key, value) = Self::get_key_value(val).ok_or_else(|| { + ctx.construct_type_error( + "iterable for Map should have array-like objects", + ) + })?; + map.insert(key, value); + } + map + } else { + return Err(ctx.construct_type_error( + "iterable for Map should have array-like objects", + )); + } + } + _ => { + return Err( + ctx.construct_type_error("iterable for Map should have array-like objects") + ) + } + }, + }; + + // finally create length property + Self::set_size(this, data.len()); + + this.set_data(ObjectData::Map(data)); + + Ok(this.clone()) + } + + /// Initialise the `Map` object on the global object. + pub(crate) fn init(global: &Value) -> (&str, Value) { + let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let prototype = Value::new_object(Some(global)); + + make_builtin_fn(Self::set, "set", &prototype, 2); + make_builtin_fn(Self::delete, "delete", &prototype, 1); + make_builtin_fn(Self::get, "get", &prototype, 1); + make_builtin_fn(Self::clear, "clear", &prototype, 0); + make_builtin_fn(Self::has, "has", &prototype, 1); + make_builtin_fn(Self::for_each, "forEach", &prototype, 1); + + let map_object = make_constructor_fn( + Self::NAME, + Self::LENGTH, + Self::make_map, + global, + prototype, + true, + false, + ); + + (Self::NAME, map_object) + } +} diff --git a/boa/src/builtins/map/ordered_map.rs b/boa/src/builtins/map/ordered_map.rs new file mode 100644 index 0000000000..d29f9b556b --- /dev/null +++ b/boa/src/builtins/map/ordered_map.rs @@ -0,0 +1,144 @@ +use gc::{custom_trace, Finalize, Trace}; +use indexmap::{map::IntoIter, map::Iter, map::IterMut, IndexMap}; +use std::collections::hash_map::RandomState; +use std::fmt::Debug; +use std::hash::{BuildHasher, Hash}; + +/// A newtype wrapping indexmap::IndexMap +#[derive(Clone)] +pub struct OrderedMap(IndexMap) +where + K: Hash + Eq; + +impl Finalize for OrderedMap {} +unsafe impl Trace for OrderedMap { + custom_trace!(this, { + for (k, v) in this.0.iter() { + mark(k); + mark(v); + } + }); +} + +impl Debug for OrderedMap { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + self.0.fmt(formatter) + } +} + +impl Default for OrderedMap { + fn default() -> Self { + Self::new() + } +} + +impl OrderedMap +where + K: Hash + Eq, +{ + pub fn new() -> Self { + OrderedMap(IndexMap::new()) + } + + pub fn with_capacity(capacity: usize) -> Self { + OrderedMap(IndexMap::with_capacity(capacity)) + } + + /// Return the number of key-value pairs in the map. + /// + /// Computes in **O(1)** time. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns true if the map contains no elements. + /// + /// Computes in **O(1)** time. + pub fn is_empty(&self) -> bool { + self.0.len() == 0 + } + + /// Insert a key-value pair in the map. + /// + /// If an equivalent key already exists in the map: the key remains and + /// retains in its place in the order, its corresponding value is updated + /// with `value` and the older value is returned inside `Some(_)`. + /// + /// If no equivalent key existed in the map: the new key-value pair is + /// inserted, last in order, and `None` is returned. + /// + /// Computes in **O(1)** time (amortized average). + pub fn insert(&mut self, key: K, value: V) -> Option { + self.0.insert(key, value) + } + + /// Remove the key-value pair equivalent to `key` and return + /// its value. + /// + /// Like `Vec::remove`, the pair is removed by shifting all of the + /// elements that follow it, preserving their relative order. + /// **This perturbs the index of all of those elements!** + /// + /// Return `None` if `key` is not in map. + /// + /// Computes in **O(n)** time (average). + pub fn remove(&mut self, key: &K) -> Option { + self.0.shift_remove(key) + } + + /// Return a reference to the value stored for `key`, if it is present, + /// else `None`. + /// + /// Computes in **O(1)** time (average). + pub fn get(&self, key: &K) -> Option<&V> { + self.0.get(key) + } + + /// Return an iterator over the key-value pairs of the map, in their order + pub fn iter(&self) -> Iter<'_, K, V> { + self.0.iter() + } + + /// Return `true` if an equivalent to `key` exists in the map. + /// + /// Computes in **O(1)** time (average). + pub fn contains_key(&self, key: &K) -> bool { + self.0.contains_key(key) + } +} + +impl<'a, K, V, S> IntoIterator for &'a OrderedMap +where + K: Hash + Eq, + S: BuildHasher, +{ + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'a, K, V, S> IntoIterator for &'a mut OrderedMap +where + K: Hash + Eq, + S: BuildHasher, +{ + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} + +impl IntoIterator for OrderedMap +where + K: Hash + Eq, + S: BuildHasher, +{ + type Item = (K, V); + type IntoIter = IntoIter; + fn into_iter(self) -> IntoIter { + self.0.into_iter() + } +} diff --git a/boa/src/builtins/map/tests.rs b/boa/src/builtins/map/tests.rs new file mode 100644 index 0000000000..2627737b89 --- /dev/null +++ b/boa/src/builtins/map/tests.rs @@ -0,0 +1,231 @@ +use crate::{exec::Interpreter, forward, realm::Realm}; + +#[test] +fn construct_empty() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + var empty = new Map(); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "empty.size"); + assert_eq!(result, "0"); +} + +#[test] +fn construct_from_array() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.size"); + assert_eq!(result, "2"); +} + +#[test] +fn clone() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let original = new Map([["1", "one"], ["2", "two"]]); + let clone = new Map(original); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "clone.size"); + assert_eq!(result, "2"); + let result = forward( + &mut engine, + r#" + original.set("3", "three"); + original.size"#, + ); + assert_eq!(result, "3"); + let result = forward(&mut engine, "clone.size"); + assert_eq!(result, "2"); +} + +#[test] +fn merge() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let first = new Map([["1", "one"], ["2", "two"]]); + let second = new Map([["2", "second two"], ["3", "three"]]); + let third = new Map([["4", "four"], ["5", "five"]]); + let merged1 = new Map([...first, ...second]); + let merged2 = new Map([...second, ...third]); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "merged1.size"); + assert_eq!(result, "3"); + let result = forward(&mut engine, "merged1.get('2')"); + assert_eq!(result, "second two"); + let result = forward(&mut engine, "merged2.size"); + assert_eq!(result, "4"); +} + +#[test] +fn get() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.get('1')"); + assert_eq!(result, "one"); + let result = forward(&mut engine, "map.get('2')"); + assert_eq!(result, "two"); + let result = forward(&mut engine, "map.get('3')"); + assert_eq!(result, "undefined"); + let result = forward(&mut engine, "map.get()"); + assert_eq!(result, "undefined"); +} + +#[test] +fn set() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map(); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.set()"); + assert_eq!(result, "Map { undefined → undefined }"); + let result = forward(&mut engine, "map.set('1', 'one')"); + assert_eq!(result, "Map { undefined → undefined, 1 → one }"); + let result = forward(&mut engine, "map.set('2')"); + assert_eq!( + result, + "Map { undefined → undefined, 1 → one, 2 → undefined }" + ); +} + +#[test] +fn clear() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + map.clear(); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.size"); + assert_eq!(result, "0"); +} + +#[test] +fn delete() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.delete('1')"); + assert_eq!(result, "true"); + let result = forward(&mut engine, "map.size"); + assert_eq!(result, "1"); + let result = forward(&mut engine, "map.delete('1')"); + assert_eq!(result, "false"); +} + +#[test] +fn has() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([["1", "one"]]); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.has()"); + assert_eq!(result, "false"); + let result = forward(&mut engine, "map.has('1')"); + assert_eq!(result, "true"); + let result = forward(&mut engine, "map.has('2')"); + assert_eq!(result, "false"); +} + +#[test] +fn for_each() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([[1, 5], [2, 10], [3, 15]]); + let valueSum = 0; + let keySum = 0; + let sizeSum = 0; + function callingCallback(value, key, map) { + valueSum += value; + keySum += key; + sizeSum += map.size; + } + map.forEach(callingCallback); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "valueSum"), "30"); + assert_eq!(forward(&mut engine, "keySum"), "6"); + assert_eq!(forward(&mut engine, "sizeSum"), "9"); +} + +#[test] +fn modify_key() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let obj = new Object(); + let map = new Map([[obj, "one"]]); + obj.field = "Value"; + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map.get(obj)"); + assert_eq!(result, "one"); +} + +#[test] +fn order() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map([[1, "one"]]); + map.set(2, "two"); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map"); + assert_eq!(result, "Map { 1 → one, 2 → two }"); + let result = forward(&mut engine, "map.set(1, \"five\");map"); + assert_eq!(result, "Map { 1 → five, 2 → two }"); + let result = forward(&mut engine, "map.set();map"); + assert_eq!(result, "Map { 1 → five, 2 → two, undefined → undefined }"); + let result = forward(&mut engine, "map.delete(2);map"); + assert_eq!(result, "Map { 1 → five, undefined → undefined }"); + let result = forward(&mut engine, "map.set(2, \"two\");map"); + assert_eq!(result, "Map { 1 → five, undefined → undefined, 2 → two }"); +} + +#[test] +fn recursive_display() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = r#" + let map = new Map(); + let array = new Array([map]); + map.set("y", map); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "map"); + assert_eq!(result, "Map { y → Map(1) }"); + let result = forward(&mut engine, "map.set(\"z\", array)"); + assert_eq!(result, "Map { y → Map(2), z → Array(1) }"); +} + +#[test] +#[should_panic] +fn not_a_function() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let init = "let map = Map()"; + forward(&mut engine, init); +} diff --git a/boa/src/builtins/mod.rs b/boa/src/builtins/mod.rs index d5a4f9cf41..127426153a 100644 --- a/boa/src/builtins/mod.rs +++ b/boa/src/builtins/mod.rs @@ -9,6 +9,7 @@ pub mod function; pub mod global_this; pub mod infinity; pub mod json; +pub mod map; pub mod math; pub mod nan; pub mod number; @@ -29,6 +30,7 @@ pub(crate) use self::{ global_this::GlobalThis, infinity::Infinity, json::Json, + map::Map, math::Math, nan::NaN, number::Number, @@ -50,6 +52,7 @@ pub fn init(global: &Value) { BigInt::init, Boolean::init, Json::init, + Map::init, Math::init, Number::init, RegExp::init, diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index 788901250c..6518fba2a7 100644 --- a/boa/src/builtins/number/mod.rs +++ b/boa/src/builtins/number/mod.rs @@ -769,6 +769,7 @@ impl Number { global, prototype, true, + true, ); make_builtin_fn(Self::number_is_finite, "isFinite", &number_object, 1); diff --git a/boa/src/builtins/object/mod.rs b/boa/src/builtins/object/mod.rs index 45cfbaa65c..4d2b02b1f1 100644 --- a/boa/src/builtins/object/mod.rs +++ b/boa/src/builtins/object/mod.rs @@ -16,6 +16,7 @@ use crate::{ builtins::{ function::Function, + map::ordered_map::OrderedMap, property::Property, value::{RcBigInt, RcString, RcSymbol, ResultValue, Value}, BigInt, @@ -67,6 +68,7 @@ pub struct Object { #[derive(Debug, Trace, Finalize, Clone)] pub enum ObjectData { Array, + Map(OrderedMap), BigInt(RcBigInt), Boolean(bool), Function(Function), @@ -85,6 +87,7 @@ impl Display for ObjectData { match self { Self::Function(_) => "Function", Self::Array => "Array", + Self::Map(_) => "Map", Self::String(_) => "String", Self::Symbol(_) => "Symbol", Self::Error => "Error", @@ -251,6 +254,28 @@ impl Object { } } + /// Checks if it is a `Map` object.pub + #[inline] + pub fn is_map(&self) -> bool { + matches!(self.data, ObjectData::Map(_)) + } + + #[inline] + pub fn as_map_ref(&self) -> Option<&OrderedMap> { + match self.data { + ObjectData::Map(ref map) => Some(map), + _ => None, + } + } + + #[inline] + pub fn as_map_mut(&mut self) -> Option<&mut OrderedMap> { + match &mut self.data { + ObjectData::Map(map) => Some(map), + _ => None, + } + } + /// Checks if it a `String` object. #[inline] pub fn is_string(&self) -> bool { @@ -548,7 +573,7 @@ pub fn init(global: &Value) -> (&str, Value) { ); make_builtin_fn(to_string, "toString", &prototype, 0); - let object = make_constructor_fn("Object", 1, make_object, global, prototype, true); + let object = make_constructor_fn("Object", 1, make_object, global, prototype, true, true); // static methods of the builtin Object make_builtin_fn(create, "create", &object, 2); diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index fb6eca2e7b..cf96bfb96e 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -486,6 +486,7 @@ impl RegExp { global, prototype, true, + true, ) } diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index fa0e00eaf9..3819fcb634 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -1042,6 +1042,7 @@ impl String { global, prototype, true, + true, ); (Self::NAME, string_object) diff --git a/boa/src/builtins/symbol/mod.rs b/boa/src/builtins/symbol/mod.rs index 0a615378d4..6548963db8 100644 --- a/boa/src/builtins/symbol/mod.rs +++ b/boa/src/builtins/symbol/mod.rs @@ -115,6 +115,7 @@ impl Symbol { global, prototype, false, + true, ); (Self::NAME, symbol_object) diff --git a/boa/src/builtins/value/display.rs b/boa/src/builtins/value/display.rs index f2bb3bada3..0843a5a2ce 100644 --- a/boa/src/builtins/value/display.rs +++ b/boa/src/builtins/value/display.rs @@ -58,7 +58,7 @@ macro_rules! print_obj_value { }; } -pub(crate) fn log_string_from(x: &Value, print_internals: bool) -> String { +pub(crate) fn log_string_from(x: &Value, print_internals: bool, print_children: bool) -> String { match x { // We don't want to print private (compiler) or prototype properties Value::Object(ref v) => { @@ -78,29 +78,63 @@ pub(crate) fn log_string_from(x: &Value, print_internals: bool) -> String { .expect("Could not borrow value"), ); - if len == 0 { - return String::from("[]"); + if print_children { + if len == 0 { + return String::from("[]"); + } + + let arr = (0..len) + .map(|i| { + // Introduce recursive call to stringify any objects + // which are part of the Array + log_string_from( + &v.borrow() + .properties() + .get(i.to_string().as_str()) + .unwrap() + .value + .clone() + .expect("Could not borrow value"), + print_internals, + false, + ) + }) + .collect::>() + .join(", "); + + format!("[ {} ]", arr) + } else { + format!("Array({})", len) + } + } + ObjectData::Map(ref map) => { + let size = i32::from( + &v.borrow() + .properties() + .get("size") + .unwrap() + .value + .clone() + .expect("Could not borrow value"), + ); + if size == 0 { + return String::from("Map(0)"); } - let arr = (0..len) - .map(|i| { - // Introduce recursive call to stringify any objects - // which are part of the Array - log_string_from( - &v.borrow() - .properties() - .get(i.to_string().as_str()) - .unwrap() - .value - .clone() - .expect("Could not borrow value"), - print_internals, - ) - }) - .collect::>() - .join(", "); - - format!("[ {} ]", arr) + if print_children { + let mappings = map + .iter() + .map(|(key, value)| { + let key = log_string_from(key, print_internals, false); + let value = log_string_from(value, print_internals, false); + format!("{} → {}", key, value) + }) + .collect::>() + .join(", "); + format!("Map {{ {} }}", mappings) + } else { + format!("Map({})", size) + } } _ => display_obj(&x, print_internals), } @@ -178,7 +212,7 @@ impl Display for Value { }, Self::String(ref v) => write!(f, "{}", v), Self::Rational(v) => format_rational(*v, f), - Self::Object(_) => write!(f, "{}", log_string_from(self, true)), + Self::Object(_) => write!(f, "{}", log_string_from(self, true, true)), Self::Integer(v) => write!(f, "{}", v), Self::BigInt(ref num) => write!(f, "{}n", num), } diff --git a/boa/src/exec/mod.rs b/boa/src/exec/mod.rs index 0f8695b470..389a08ab0c 100644 --- a/boa/src/exec/mod.rs +++ b/boa/src/exec/mod.rs @@ -38,6 +38,7 @@ use crate::{ }, BoaProfiler, }; +use std::borrow::Borrow; use std::convert::TryFrom; use std::ops::Deref; @@ -377,6 +378,38 @@ impl Interpreter { .collect(); return Ok(values); } + // Check if object is a Map + else if let ObjectData::Map(ref map) = x.deref().borrow().data { + let values = map + .borrow() + .iter() + .map(|(key, value)| { + // Construct a new array containing the key-value pair + let array = Value::new_object(Some( + &self + .realm() + .environment + .get_global_object() + .expect("Could not get global object"), + )); + array.set_data(ObjectData::Array); + array.borrow().set_internal_slot( + INSTANCE_PROTOTYPE, + self.realm() + .environment + .get_binding_value("Array") + .expect("Array was not initialized") + .borrow() + .get_field(PROTOTYPE), + ); + array.borrow().set_field("0", key); + array.borrow().set_field("1", value); + array.borrow().set_field("length", Value::from(2)); + array + }) + .collect(); + return Ok(values); + } return Err(()); }