From 868594273d654940aca742678dbcc6d0d077b5bc Mon Sep 17 00:00:00 2001 From: RageKnify Date: Wed, 3 Feb 2021 02:56:49 +0000 Subject: [PATCH] Feat: Implement Set builtin object Includes change to Value::hash so that 1i32 == 1f64 closes #400, blocked by #1109 --- boa/src/builtins/iterable/mod.rs | 8 + boa/src/builtins/mod.rs | 4 + boa/src/builtins/set/mod.rs | 350 +++++++++++++++++++++++++++ boa/src/builtins/set/ordered_set.rs | 130 ++++++++++ boa/src/builtins/set/set_iterator.rs | 149 ++++++++++++ boa/src/builtins/set/tests.rs | 248 +++++++++++++++++++ boa/src/object/mod.rs | 35 +++ boa/src/value/display.rs | 21 ++ boa/src/value/hash.rs | 2 +- 9 files changed, 946 insertions(+), 1 deletion(-) create mode 100644 boa/src/builtins/set/mod.rs create mode 100644 boa/src/builtins/set/ordered_set.rs create mode 100644 boa/src/builtins/set/set_iterator.rs create mode 100644 boa/src/builtins/set/tests.rs diff --git a/boa/src/builtins/iterable/mod.rs b/boa/src/builtins/iterable/mod.rs index 9bd12183910..9f474dd9a8f 100644 --- a/boa/src/builtins/iterable/mod.rs +++ b/boa/src/builtins/iterable/mod.rs @@ -3,6 +3,7 @@ use crate::{ builtins::ArrayIterator, builtins::ForInIterator, builtins::MapIterator, + builtins::SetIterator, object::{GcObject, ObjectInitializer}, property::{Attribute, DataDescriptor}, BoaProfiler, Context, Result, Value, @@ -12,6 +13,7 @@ use crate::{ pub struct IteratorPrototypes { iterator_prototype: GcObject, array_iterator: GcObject, + set_iterator: GcObject, string_iterator: GcObject, map_iterator: GcObject, for_in_iterator: GcObject, @@ -25,6 +27,7 @@ impl IteratorPrototypes { context, iterator_prototype.clone().into(), ), + set_iterator: SetIterator::create_prototype(context, iterator_prototype.clone().into()), string_iterator: StringIterator::create_prototype( context, iterator_prototype.clone().into(), @@ -48,6 +51,11 @@ impl IteratorPrototypes { self.iterator_prototype.clone() } + #[inline] + pub fn set_iterator(&self) -> GcObject { + self.set_iterator.clone() + } + #[inline] pub fn string_iterator(&self) -> GcObject { self.string_iterator.clone() diff --git a/boa/src/builtins/mod.rs b/boa/src/builtins/mod.rs index 12eec3893e3..f7b8240dfcf 100644 --- a/boa/src/builtins/mod.rs +++ b/boa/src/builtins/mod.rs @@ -18,6 +18,7 @@ pub mod nan; pub mod number; pub mod object; pub mod regexp; +pub mod set; pub mod string; pub mod symbol; pub mod undefined; @@ -40,6 +41,8 @@ pub(crate) use self::{ object::for_in_iterator::ForInIterator, object::Object as BuiltInObjectObject, regexp::RegExp, + set::set_iterator::SetIterator, + set::Set, string::String, symbol::Symbol, undefined::Undefined, @@ -76,6 +79,7 @@ pub fn init(context: &mut Context) { Date::init, Map::init, Number::init, + Set::init, String::init, RegExp::init, Symbol::init, diff --git a/boa/src/builtins/set/mod.rs b/boa/src/builtins/set/mod.rs new file mode 100644 index 00000000000..aeb1a30867b --- /dev/null +++ b/boa/src/builtins/set/mod.rs @@ -0,0 +1,350 @@ +use crate::{ + builtins::{iterable::get_iterator, BuiltIn}, + object::{ConstructorBuilder, FunctionBuilder, ObjectData, PROTOTYPE}, + property::Attribute, + BoaProfiler, Context, Result, Value, +}; +use ordered_set::OrderedSet; + +pub mod set_iterator; +use set_iterator::{SetIterationKind, SetIterator}; + +pub mod ordered_set; +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone)] +pub(crate) struct Set(OrderedSet); + +impl BuiltIn for Set { + const NAME: &'static str = "Set"; + + fn attribute() -> Attribute { + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE + } + + fn init(context: &mut Context) -> (&'static str, Value, Attribute) { + let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + + let size_getter = FunctionBuilder::new(context, Self::size_getter) + .callable(true) + .constructable(false) + .build(); + + let iterator_symbol = context.well_known_symbols().iterator_symbol(); + + let to_string_tag = context.well_known_symbols().to_string_tag_symbol(); + + let values_function = FunctionBuilder::new(context, Self::values) + .name("values") + .length(0) + .callable(true) + .constructable(false) + .build(); + + let map_object = ConstructorBuilder::new(context, Self::constructor) + .name(Self::NAME) + .length(Self::LENGTH) + .method(Self::add, "add", 1) + .method(Self::clear, "clear", 0) + .method(Self::delete, "delete", 1) + .method(Self::entries, "entries", 0) + .method(Self::for_each, "forEach", 0) + .method(Self::has, "has", 1) + .property( + "keys", + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .accessor("size", Some(size_getter), None, Attribute::default()) + .property( + "values", + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + iterator_symbol, + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property(to_string_tag, "Set", Attribute::CONFIGURABLE) + .build(); + + (Self::NAME, map_object.into(), Self::attribute()) + } +} + +impl Set { + pub(crate) const LENGTH: usize = 0; + + /// Create a new set + pub(crate) fn constructor( + new_target: &Value, + args: &[Value], + context: &mut Context, + ) -> Result { + // 1 + if new_target.is_undefined() { + return context.throw_type_error("Constructor Set requires 'new'"); + } + + // 2 + let set_prototype = context + .global_object() + .clone() + .get( + &"Set".into(), + context.global_object().clone().into(), + context, + )? + .get_field(PROTOTYPE, context)? + .as_object() + .expect("'Set' global property should be an object"); + let prototype = new_target + .as_object() + .and_then(|obj| { + obj.get(&PROTOTYPE.into(), obj.clone().into(), context) + .map(|o| o.as_object()) + .transpose() + }) + .transpose()? + .unwrap_or(set_prototype); + + let mut obj = context.construct_object(); + obj.set_prototype_instance(prototype.into()); + + let set = Value::from(obj); + // 3 + set.set_data(ObjectData::Set(OrderedSet::default())); + + let iterable = args.get(0).cloned().unwrap_or_default(); + // 4 + if iterable.is_null_or_undefined() { + return Ok(set); + } + + // 5 + let adder = set.get_field("add", context)?; + + // 6 + if !adder.is_function() { + return context.throw_type_error("'add' of 'newTarget' is not a function"); + } + + // 7 + let iterator_record = get_iterator(context, iterable)?; + + // 8.a + let mut next = iterator_record.next(context)?; + + // 8 + while !next.is_done() { + // c + let next_value = next.value(); + + // d, e + context.call(&adder, &set, &[next_value])?; + + next = iterator_record.next(context)? + } + + // 8.b + Ok(set) + } + + /// `Set.prototype.add( value )` + /// + /// This method adds an entry with value into the set. Returns the set object + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.add + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add + pub(crate) fn add(this: &Value, args: &[Value], context: &mut Context) -> Result { + let value = args.get(0).cloned().unwrap_or_default(); + + if let Some(object) = this.as_object() { + if let Some(set) = object.borrow_mut().as_set_mut() { + set.add(value); + } else { + return context.throw_type_error("'this' is not a Set"); + } + } else { + return context.throw_type_error("'this' is not a Set"); + }; + + Ok(this.clone()) + } + + /// `Set.prototype.clear( )` + /// + /// This method removes all entries from the set. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.clear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear + pub(crate) fn clear(this: &Value, _: &[Value], context: &mut Context) -> Result { + if let Some(object) = this.as_object() { + if object.borrow_mut().is_set() { + this.set_data(ObjectData::Set(OrderedSet::new())); + Ok(Value::Undefined) + } else { + context.throw_type_error("'this' is not a Set") + } + } else { + context.throw_type_error("'this' is not a Set") + } + } + + /// `Set.prototype.delete( value )` + /// + /// This method removes the entry for the given value 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-set.prototype.delete + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete + pub(crate) fn delete(this: &Value, args: &[Value], context: &mut Context) -> Result { + let value = args.get(0).cloned().unwrap_or_default(); + + let res = if let Some(object) = this.as_object() { + if let Some(set) = object.borrow_mut().as_set_mut() { + let res = set.delete(&value); + res + } else { + return context.throw_type_error("'this' is not a Set"); + } + } else { + return context.throw_type_error("'this' is not a Set"); + }; + + Ok(res.into()) + } + + /// `Set.prototype.entries( )` + /// + /// This method returns an iterator over the entries of the set + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.entries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries + pub(crate) fn entries(this: &Value, _: &[Value], context: &mut Context) -> Result { + SetIterator::create_set_iterator(context, this.clone(), SetIterationKind::KeyAndValue) + } + + /// `Set.prototype.forEach( callbackFn [ , thisArg ] )` + /// + /// This method executes the provided callback function for each value in the set + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.foreach + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/foreach + pub(crate) fn for_each(this: &Value, args: &[Value], context: &mut Context) -> Result { + if args.is_empty() { + return Err(Value::from("Missing argument for Set.prototype.forEach")); + } + + let callback_arg = &args[0]; + let this_arg = args.get(1).cloned().unwrap_or_else(Value::undefined); + + let mut index = 0; + + while index < Set::get_size(this, context)? { + let arguments = if let Value::Object(ref object) = this { + let object = object.borrow(); + if let Some(set) = object.as_set_ref() { + if let Some(value) = set.get_index(index) { + Some([value.clone(), value.clone(), this.clone()]) + } else { + None + } + } else { + return context.throw_type_error("'this' is not a Set"); + } + } else { + return context.throw_type_error("'this' is not a Set"); + }; + + if let Some(arguments) = arguments { + context.call(callback_arg, &this_arg, &arguments)?; + } + + index += 1; + } + + 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], context: &mut Context) -> Result { + let undefined = Value::Undefined; + let value = match args.len() { + 0 => &undefined, + _ => &args[0], + }; + + if let Value::Object(ref object) = this { + let object = object.borrow(); + if let Some(set) = object.as_set_ref() { + return Ok(set.contains(value).into()); + } + } + + Err(context.construct_type_error("'this' is not a Set")) + } + + /// `Set.prototype.values( )` + /// + /// This method returns an iterator over the values of the set + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values + pub(crate) fn values(this: &Value, _: &[Value], context: &mut Context) -> Result { + SetIterator::create_set_iterator(context, this.clone(), SetIterationKind::Value) + } + + fn size_getter(this: &Value, _: &[Value], context: &mut Context) -> Result { + Set::get_size(this, context).map(Value::from) + } + + /// Helper function to get the size of the set. + fn get_size(set: &Value, context: &mut Context) -> Result { + if let Value::Object(ref object) = set { + let object = object.borrow(); + if let Some(set) = object.as_set_ref() { + Ok(set.size()) + } else { + Err(context.construct_type_error("'this' is not a Set")) + } + } else { + Err(context.construct_type_error("'this' is not a Set")) + } + } +} diff --git a/boa/src/builtins/set/ordered_set.rs b/boa/src/builtins/set/ordered_set.rs new file mode 100644 index 00000000000..c83c57d465f --- /dev/null +++ b/boa/src/builtins/set/ordered_set.rs @@ -0,0 +1,130 @@ +use crate::gc::{custom_trace, Finalize, Trace}; +use indexmap::{ + set::{IntoIter, Iter}, + IndexSet, +}; +use std::{ + collections::hash_map::RandomState, + fmt::Debug, + hash::{BuildHasher, Hash}, +}; + +/// A newtype wrapping indexmap::IndexSet +#[derive(Clone)] +pub struct OrderedSet(IndexSet) +where + V: Hash + Eq; + +impl Finalize for OrderedSet {} +unsafe impl Trace for OrderedSet { + custom_trace!(this, { + for v in this.0.iter() { + mark(v); + } + }); +} + +impl Debug for OrderedSet { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + self.0.fmt(formatter) + } +} + +impl Default for OrderedSet { + fn default() -> Self { + Self::new() + } +} + +impl OrderedSet +where + V: Hash + Eq, +{ + pub fn new() -> Self { + OrderedSet(IndexSet::new()) + } + + pub fn with_capacity(capacity: usize) -> Self { + OrderedSet(IndexSet::with_capacity(capacity)) + } + + /// Return the number of key-value pairs in the map. + /// + /// Computes in **O(1)** time. + pub fn size(&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 value pair in the set. + /// + /// If an equivalent value already exists in the set: ??? + /// + /// If no equivalent value existed in the set: the new value is + /// inserted, last in order, and false + /// + /// Computes in **O(1)** time (amortized average). + pub fn add(&mut self, value: V) -> bool { + self.0.insert(value) + } + + /// Delete the `value` from the set and return true if successful + /// + /// Return `false` if `value` is not in map. + /// + /// Computes in **O(n)** time (average). + pub fn delete(&mut self, value: &V) -> bool { + self.0.shift_remove(value) + } + + /// Checks if a given value is present in the set + /// + /// Return `true` if `value` is present in set, false otherwise. + /// + /// Computes in **O(n)** time (average). + pub fn contains(&self, value: &V) -> bool { + self.0.contains(value) + } + + /// Get a key-value pair by index + /// Valid indices are 0 <= index < self.len() + /// Computes in O(1) time. + pub fn get_index(&self, index: usize) -> Option<&V> { + self.0.get_index(index) + } + + /// Return an iterator over the values of the set, in their order + pub fn iter(&self) -> Iter<'_, V> { + self.0.iter() + } +} + +impl<'a, V, S> IntoIterator for &'a OrderedSet +where + V: Hash + Eq, + S: BuildHasher, +{ + type Item = &'a V; + type IntoIter = Iter<'a, V>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for OrderedSet +where + V: Hash + Eq, + S: BuildHasher, +{ + type Item = V; + type IntoIter = IntoIter; + fn into_iter(self) -> IntoIter { + self.0.into_iter() + } +} diff --git a/boa/src/builtins/set/set_iterator.rs b/boa/src/builtins/set/set_iterator.rs new file mode 100644 index 00000000000..751da3de15a --- /dev/null +++ b/boa/src/builtins/set/set_iterator.rs @@ -0,0 +1,149 @@ +use crate::{ + builtins::function::make_builtin_fn, + builtins::iterable::create_iter_result_object, + builtins::Array, + builtins::Value, + object::{GcObject, ObjectData}, + property::{Attribute, DataDescriptor}, + BoaProfiler, Context, Result, +}; +use gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Finalize, Trace)] +pub enum SetIterationKind { + Value, + KeyAndValue, +} + +/// The Set Iterator object represents an iteration over a set. It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-set-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct SetIterator { + iterated_set: Value, + set_next_index: usize, + set_iteration_kind: SetIterationKind, +} + +impl SetIterator { + pub(crate) const NAME: &'static str = "SetIterator"; + + fn new(set: Value, kind: SetIterationKind) -> Self { + SetIterator { + iterated_set: set, + set_next_index: 0, + set_iteration_kind: kind, + } + } + + /// Abstract operation CreateSetIterator( set, kind ) + /// + /// Creates a new iterator over the given set. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-createsetiterator + pub(crate) fn create_set_iterator( + context: &Context, + set: Value, + kind: SetIterationKind, + ) -> Result { + let set_iterator = Value::new_object(context); + set_iterator.set_data(ObjectData::SetIterator(Self::new(set, kind))); + set_iterator + .as_object() + .expect("set iterator object") + .set_prototype_instance(context.iterator_prototypes().set_iterator().into()); + Ok(set_iterator) + } + + /// %SetIteratorPrototype%.next( ) + /// + /// Advances the iterator and gets the next result in the set. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%setiteratorprototype%.next + pub(crate) fn next(this: &Value, _: &[Value], context: &mut Context) -> Result { + if let Value::Object(ref object) = this { + let mut object = object.borrow_mut(); + if let Some(set_iterator) = object.as_set_iterator_mut() { + let m = &set_iterator.iterated_set; + let mut index = set_iterator.set_next_index; + let item_kind = &set_iterator.set_iteration_kind; + + if set_iterator.iterated_set.is_undefined() { + return Ok(create_iter_result_object(context, Value::undefined(), true)); + } + + if let Value::Object(ref object) = m { + if let Some(entries) = object.borrow().as_set_ref() { + let num_entries = entries.size(); + while index < num_entries { + let e = entries.get_index(index); + index += 1; + set_iterator.set_next_index = index; + if let Some(value) = e { + match item_kind { + SetIterationKind::Value => { + return Ok(create_iter_result_object( + context, + value.clone(), + false, + )); + } + SetIterationKind::KeyAndValue => { + let result = Array::construct_array( + &Array::new_array(context), + &[value.clone(), value.clone()], + context, + )?; + return Ok(create_iter_result_object( + context, result, false, + )); + } + } + } + } + } else { + return Err(context.construct_type_error("'this' is not a Set")); + } + } else { + return Err(context.construct_type_error("'this' is not a Set")); + } + + set_iterator.iterated_set = Value::undefined(); + Ok(create_iter_result_object(context, Value::undefined(), true)) + } else { + context.throw_type_error("`this` is not an SetIterator") + } + } else { + context.throw_type_error("`this` is not an SetIterator") + } + } + + /// Create the %SetIteratorPrototype% object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%setiteratorprototype%-object + pub(crate) fn create_prototype(context: &mut Context, iterator_prototype: Value) -> GcObject { + let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let mut set_iterator = context.construct_object(); + make_builtin_fn(Self::next, "next", &set_iterator, 0, context); + set_iterator.set_prototype_instance(iterator_prototype); + + let to_string_tag = context.well_known_symbols().to_string_tag_symbol(); + let to_string_tag_property = DataDescriptor::new("Set Iterator", Attribute::CONFIGURABLE); + set_iterator.insert(to_string_tag, to_string_tag_property); + set_iterator + } +} diff --git a/boa/src/builtins/set/tests.rs b/boa/src/builtins/set/tests.rs new file mode 100644 index 00000000000..611a4ef1580 --- /dev/null +++ b/boa/src/builtins/set/tests.rs @@ -0,0 +1,248 @@ +use crate::{forward, Context}; + +#[test] +fn construct_empty() { + let mut context = Context::new(); + let init = r#" + var empty = new Set(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "empty.size"); + assert_eq!(result, "0"); +} + +#[test] +fn construct_from_array() { + let mut context = Context::new(); + let init = r#" + let set = new Set(["one", "two"]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.size"); + assert_eq!(result, "2"); +} + +#[test] +fn clone() { + let mut context = Context::new(); + let init = r#" + let original = new Set(["one", "two"]); + let clone = new Set(original); + "#; + forward(&mut context, init); + let result = forward(&mut context, "clone.size"); + assert_eq!(result, "2"); + let result = forward( + &mut context, + r#" + original.add("three"); + original.size"#, + ); + assert_eq!(result, "3"); + let result = forward(&mut context, "clone.size"); + assert_eq!(result, "2"); +} + +#[test] +fn symbol_iterator() { + let mut context = Context::new(); + let init = r#" + const set1 = new Set(); + set1.add('foo'); + set1.add('bar'); + const iterator = set1[Symbol.iterator](); + let item1 = iterator.next(); + let item2 = iterator.next(); + let item3 = iterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn entries() { + let mut context = Context::new(); + let init = r#" + const set1 = new Set(); + set1.add('foo'); + set1.add('bar'); + const entriesIterator = set1.entries(); + let item1 = entriesIterator.next(); + let item2 = entriesIterator.next(); + let item3 = entriesIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item1.value[0]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.value[1]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item2.value[0]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.value[1]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn merge() { + let mut context = Context::new(); + let init = r#" + let first = new Set(["one", "two"]); + let second = new Set(["three", "four"]); + let third = new Set(["four", "five"]); + let merged1 = new Set([...first, ...second]); + let merged2 = new Set([...second, ...third]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "merged1.size"); + assert_eq!(result, "4"); + let result = forward(&mut context, "merged2.size"); + assert_eq!(result, "3"); +} + +#[test] +fn clear() { + let mut context = Context::new(); + let init = r#" + let set = new Set(["one", "two"]); + set.clear(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.size"); + assert_eq!(result, "0"); +} + +#[test] +fn delete() { + let mut context = Context::new(); + let init = r#" + let set = new Set(["one", "two"]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.delete('one')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set.size"); + assert_eq!(result, "1"); + let result = forward(&mut context, "set.delete('one')"); + assert_eq!(result, "false"); +} + +#[test] +fn has() { + let mut context = Context::new(); + let init = r#" + let set = new Set(["one", "two"]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.has('one')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set.has('two')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set.has('three')"); + assert_eq!(result, "false"); + let result = forward(&mut context, "set.has()"); + assert_eq!(result, "false"); +} + +#[test] +fn values_and_keys() { + let mut context = Context::new(); + let init = r#" + const set1 = new Set(); + set1.add('foo'); + set1.add('bar'); + const valuesIterator = set1.values(); + let item1 = valuesIterator.next(); + let item2 = valuesIterator.next(); + let item3 = valuesIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set1.values == set1.keys"); + assert_eq!(result, "true"); +} + +#[test] +fn for_each() { + let mut context = Context::new(); + let init = r#" + let set = new Set([5, 10, 15]); + let value1Sum = 0; + let value2Sum = 0; + let sizeSum = 0; + function callingCallback(value1, value2, set) { + value1Sum += value1; + value2Sum += value2; + sizeSum += set.size; + } + set.forEach(callingCallback); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "value1Sum"), "30"); + assert_eq!(forward(&mut context, "value2Sum"), "30"); + assert_eq!(forward(&mut context, "sizeSum"), "9"); +} + +#[test] +fn recursive_display() { + let mut context = Context::new(); + let init = r#" + let set = new Set(); + let array = new Array([set]); + set.add(set); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set"); + assert_eq!(result, "Set { Set(1) }"); + let result = forward(&mut context, "set.add(array)"); + assert_eq!(result, "Set { Set(2), Array(1) }"); +} + +#[test] +fn not_a_function() { + let mut context = Context::new(); + let init = r" + try { + let set = Set() + } catch(e) { + e.toString() + } + "; + assert_eq!( + forward(&mut context, init), + "\"TypeError: Constructor Set requires 'new'\"" + ); +} diff --git a/boa/src/object/mod.rs b/boa/src/object/mod.rs index da2f80a4ee0..55aa689f942 100644 --- a/boa/src/object/mod.rs +++ b/boa/src/object/mod.rs @@ -6,6 +6,8 @@ use crate::{ function::{BuiltInFunction, Function, FunctionFlags, NativeFunction}, map::map_iterator::MapIterator, map::ordered_map::OrderedMap, + set::ordered_set::OrderedSet, + set::set_iterator::SetIterator, string::string_iterator::StringIterator, BigInt, Date, RegExp, }, @@ -87,6 +89,8 @@ pub enum ObjectData { Boolean(bool), ForInIterator(ForInIterator), Function(Function), + Set(OrderedSet), + SetIterator(SetIterator), String(RcString), StringIterator(StringIterator), Number(f64), @@ -111,6 +115,8 @@ impl Display for ObjectData { Self::RegExp(_) => "RegExp", Self::Map(_) => "Map", Self::MapIterator(_) => "MapIterator", + Self::Set(_) => "Set", + Self::SetIterator(_) => "SetIterator", Self::String(_) => "String", Self::StringIterator(_) => "StringIterator", Self::Symbol(_) => "Symbol", @@ -360,6 +366,35 @@ impl Object { } } + #[inline] + pub fn is_set(&self) -> bool { + matches!(self.data, ObjectData::Set(_)) + } + + #[inline] + pub fn as_set_ref(&self) -> Option<&OrderedSet> { + match self.data { + ObjectData::Set(ref set) => Some(set), + _ => None, + } + } + + #[inline] + pub fn as_set_mut(&mut self) -> Option<&mut OrderedSet> { + match &mut self.data { + ObjectData::Set(set) => Some(set), + _ => None, + } + } + + #[inline] + pub fn as_set_iterator_mut(&mut self) -> Option<&mut SetIterator> { + match &mut self.data { + ObjectData::SetIterator(iter) => Some(iter), + _ => None, + } + } + /// Checks if it a `String` object. #[inline] pub fn is_string(&self) -> bool { diff --git a/boa/src/value/display.rs b/boa/src/value/display.rs index 801d8b22357..7e07bace64c 100644 --- a/boa/src/value/display.rs +++ b/boa/src/value/display.rs @@ -169,6 +169,27 @@ pub(crate) fn log_string_from(x: &Value, print_internals: bool, print_children: format!("Map({})", size) } } + ObjectData::Set(ref set) => { + let size = set.size(); + + if size == 0 { + return String::from("Set(0)"); + } + + if print_children { + let entries = set + .iter() + .map(|value| { + let value = log_string_from(value, print_internals, false); + format!("{}", value) + }) + .collect::>() + .join(", "); + format!("Set {{ {} }}", entries) + } else { + format!("Set({})", size) + } + } _ => display_obj(&x, print_internals), } } diff --git a/boa/src/value/hash.rs b/boa/src/value/hash.rs index 96ce6ebb5fa..c8bf15caa6a 100644 --- a/boa/src/value/hash.rs +++ b/boa/src/value/hash.rs @@ -43,7 +43,7 @@ impl Hash for Value { Self::Null => NullHashable.hash(state), Self::String(ref string) => string.hash(state), Self::Boolean(boolean) => boolean.hash(state), - Self::Integer(integer) => integer.hash(state), + Self::Integer(integer) => RationalHashable(f64::from(*integer)).hash(state), Self::BigInt(ref bigint) => bigint.hash(state), Self::Rational(rational) => RationalHashable(*rational).hash(state), Self::Symbol(ref symbol) => Hash::hash(symbol, state),