diff --git a/core/engine/src/builtins/set/mod.rs b/core/engine/src/builtins/set/mod.rs index 0d57bcf89ed..2227914461b 100644 --- a/core/engine/src/builtins/set/mod.rs +++ b/core/engine/src/builtins/set/mod.rs @@ -18,6 +18,7 @@ mod tests; pub mod ordered_set; use self::ordered_set::OrderedSet; +use super::iterable::IteratorHint; use crate::{ builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -32,11 +33,8 @@ use crate::{ }; use boa_profiler::Profiler; use num_traits::Zero; - pub(crate) use set_iterator::SetIterator; -use super::iterable::IteratorHint; - #[derive(Debug, Clone)] pub(crate) struct Set; @@ -72,6 +70,17 @@ impl IntrinsicObject for Set { .method(Self::entries, js_string!("entries"), 0) .method(Self::for_each, js_string!("forEach"), 1) .method(Self::has, js_string!("has"), 1) + .method(Self::difference, js_string!("difference"), 1) + .method(Self::intersection, js_string!("intersection"), 1) + .method(Self::is_dis_joint_from, js_string!("isDisjointFrom"), 0) + .method(Self::is_subset_of, js_string!("isSubsetOf"), 0) + .method(Self::is_superset_of, js_string!("isSupersetOf"), 0) + .method( + Self::symmetric_difference, + js_string!("symmetricDifference"), + 1, + ) + .method(Self::union, js_string!("union"), 1) .property( js_string!("keys"), values_function.clone(), @@ -491,6 +500,418 @@ impl Set { )) } + /// ` Set.prototype.difference ( other ) ` + /// + /// This method returns a new Set containing all elements that are in the current Set + /// but not in the given iterable `other`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.difference + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/difference + pub(crate) fn difference( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let S be the this value. + // 2. Perform ? RequireInternalSlot(S, [[SetData]]). + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.difference called on incompatible receiver") + .into()); + }; + // 3. Let otherRec be ? GetSetRecord(other). + // (ECMAScript 2022, 24.2.3.6 step 3) + let other = args.get_or_undefined(0); + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.difference called on incompatible receiver") + .into()); + }; + // 4. Let resultSetData be a copy of O.[[SetData]]. + let mut result_set = set.clone(); + // 5. If SetDataSize(O.[[SetData]]) ≤ otherRec.[[Size]], then: + if Self::get_size_full(this)? <= other_set.len() { + // Iterate over elements of the current set. + let elements: Vec<_> = result_set.iter().cloned().collect(); + for element in elements { + // Remove elements from resultSetData that are in otherRec. + if other_set.contains(&element) { + result_set.delete(&element); + } + } + } else { + // Otherwise, iterate over elements of the other set. + let other_elements: Vec<_> = other_set.iter().cloned().collect(); + for element in other_elements { + // Remove elements from resultSetData that are in otherRec. + result_set.delete(&element); + } + } + // 6. Return a new set with the updated resultSetData. + Ok(Self::create_set_from_list(result_set.iter().cloned(), context).into()) + } + + /// `Set.prototype.intersection ( other )` + /// + /// This method returns a new Set containing all elements that are present in both + /// the current Set and the given iterable `other`. + /// + /// It effectively computes the intersection of the two Sets. + /// + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.intersection + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection + pub(crate) fn intersection( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let S be the this value. + // Here, the variable `S` holds the value of `this`, which represents the current set over which the operation is being performed. + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.difference called on incompatible receiver") + .into()); + }; + // 2. Perform ? RequireInternalSlot(S, [[SetData]]). + // This step checks if the object calling the method has an internal data structure `[[SetData]]`. + // This is important to ensure that the object is a valid set that can be operated on. + // The error handling for this case is already done in the previous step when trying to access the internal data. + // 3. Let other be the first argument. + // We retrieve the first argument passed to the `intersection` method. This is the second set with which we want to find the intersection. + let other = args.get_or_undefined(0); + // 4. Let other_set be the second argument. + // We try to downcast the second argument into an `OrderedSet`, which is the other set with which we perform the intersection. + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.difference called on incompatible receiver") + .into()); + }; + // 5. If S or other is empty, return an empty Set. + // If either of the sets is empty, the intersection will also be empty. + // In this case, we immediately return an empty set. + if set.is_empty() || other_set.is_empty() { + return Ok(Self::create_set_from_list(set.iter().cloned(), context).into()); + } + // 6. Create an empty result set. + // We create an empty set that will hold the common elements of the two sets. + let mut result_set = OrderedSet::new(); + // 7. Let iter_set and check_set be the set with fewer elements. + // For optimization, we choose to iterate over the smaller set. This reduces the number of operations when one set is much smaller than the other. + let (iter_set, check_set) = if set.len() <= other_set.len() { + (other_set.iter(), set) + } else { + (set.iter(), other_set) + }; + // 8. Iterate through iter_set and add elements to result_set if they are contained in check_set. + // We loop through the smaller set and add the elements that are found in the larger set to the result set. + for value in iter_set { + if check_set.contains(value) { + result_set.add(value.clone()); + } + } + // 9. Return the result set. + // After the iteration, we return the result set containing the intersected elements. + Ok(Set::create_set_from_list(result_set.iter().cloned(), context).into()) + } + + /// `Set.prototype.isDisjointFrom ( other )` + /// + /// This method checks whether the current Set and the given iterable `other` have no elements in common. + /// It returns `true` if the two Sets are disjoint (i.e., they have no overlapping elements), + /// and `false` otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.isdisjointfrom + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isDisjointFrom + pub(crate) fn is_dis_joint_from( + this: &JsValue, + args: &[JsValue], + _: &mut Context, + ) -> JsResult { + // 1. Let S be the this value. + // 2. Perform ? RequireInternalSlot(S, [[SetData]]). + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isDisjointFrom called on incompatible receiver") + .into()); + }; + + // 3. Let otherRec be ? GetSetRecord(other). + let other = args.get_or_undefined(0); + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isDisjointFrom called on incompatible receiver") + .into()); + }; + + // 4. Iterate over the smaller set to check for common elements. + if Self::get_size_full(this)? <= other_set.len() { + for value in set.iter() { + if other_set.contains(value) { + return Ok(JsValue::from(false)); + } + } + } else { + for value in other_set.iter() { + if set.contains(value) { + return Ok(JsValue::from(false)); + } + } + } + + // 5. If no common elements are found, return true. + Ok(JsValue::from(true)) + } + + /// `Set.prototype.isSubsetOf ( other )` + /// + /// This method checks whether the current Set is a subset of the given iterable `other`. + /// It returns `true` if all elements of the current Set are present in the given iterable, + /// and `false` otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.issubsetof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSubsetOf + pub(crate) fn is_subset_of( + this: &JsValue, + args: &[JsValue], + _: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isSubsetOf called on incompatible receiver") + .into()); + }; + + // 2. Perform ? RequireInternalSlot(O, [[SetData]]). + // 3. Let otherRec be ? GetSetRecord(other). + let other = args.get_or_undefined(0); + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isSubsetOf called on incompatible argument") + .into()); + }; + + // 4. If SetDataSize(O.[[SetData]]) > otherRec.[[Size]], return false. + if set.len() > other_set.len() { + return Ok(JsValue::from(false)); + } + + // 5. Let thisSize be the number of elements in O.[[SetData]]. + + // 6. Let index be 0. + for value in set.iter() { + // 7. If e is not empty, then + // 8. Call the Has method of other_set to check if `value` is contained in it. + if !other_set.contains(value) { + return Ok(JsValue::from(false)); + } + } + + // 9. Return true if all elements of `this` are in `other`. + Ok(JsValue::from(true)) + } + + /// `Set.prototype.isSupersetOf ( other )` + /// + /// This method checks whether the current Set is a superset of the given iterable `other`. + /// It returns `true` if the current Set contains all elements from the given iterable, + /// and `false` otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.issupersetof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSupersetOf + pub(crate) fn is_superset_of( + this: &JsValue, + args: &[JsValue], + _: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isSupersetOf called on incompatible receiver") + .into()); + }; + + // 2. Perform ? RequireInternalSlot(O, [[SetData]]). + let other = args.get_or_undefined(0); + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isSupersetOf called on incompatible argument") + .into()); + }; + + // 3. If SetDataSize(O.[[SetData]]) < otherRec.[[Size]], return false. + if Self::get_size_full(this)? <= other_set.len() { + return Ok(JsValue::from(false)); + } + + // 4. Let thisSize be the number of elements in O.[[SetData]]. + for value in other_set.iter() { + if !set.contains(value) { + return Ok(JsValue::from(false)); + } + } + + // 5. Return true if all elements of `other` are in `this`. + Ok(JsValue::from(true)) + } + + /// ` Set.prototype.symmetricDifference(other)` + /// + /// Returns a new set containing the symmetric difference between the current set (`this`) + /// and the provided set (`other`) + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.symmerticDifference + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference + pub(crate) fn symmetric_difference( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isSupersetOf called on incompatible receiver") + .into()); + }; + + // 2. Perform ? RequireInternalSlot(O, [[SetData]]). + let other = args.get_or_undefined(0); + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.isSupersetOf called on incompatible argument") + .into()); + }; + + // 4. Let resultSetData be a copy of O.[[SetData]]. + let mut result_set = set.clone(); + + for value in other_set.iter() { + // If value is in the current set, remove it from result_set. + if set.contains(value) { + result_set.delete(value); + } else { + // Otherwise, add it to result_set. + result_set.add(value.clone()); + } + } + + // Ensure we return a valid Set object. + let result_set_obj = Self::create_set_from_list(result_set.iter().cloned(), context); + + // Return the new Set object as a JsValue. + Ok(result_set_obj.into()) + } + + /// `Set.prototype.union ( other )` + /// + /// Returns a new set containing the union of the elements in the current set (`this`) + /// and the set provided as the argument (`other`). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.union + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/union + pub(crate) fn union( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // Ensure the `this` value is a valid set object. + let Some(set) = this + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Method Set.prototype.union called on incompatible receiver") + .into()); + }; + + // Get the other set from the arguments. + let other = args.get_or_undefined(0); + let Some(other_set) = other + .as_object() + .and_then(JsObject::downcast_ref::) + else { + return Err(JsNativeError::typ() + .with_message("Argument passed to Set.prototype.union is not a Set") + .into()); + }; + + // Create a new set to store the union of the two sets. + let mut result_set = set.clone(); + + // Add all elements from the other set to the result set. + for value in other_set.iter() { + result_set.add(value.clone()); + } + + // Return a new Set object created from the union of the two sets. + Ok(Set::create_set_from_list(result_set.iter().cloned(), context).into()) + } + fn size_getter(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Self::get_size(this).map(JsValue::from) } diff --git a/core/engine/src/builtins/set/tests.rs b/core/engine/src/builtins/set/tests.rs index 50f5289ab2f..5dc1b5b1120 100644 --- a/core/engine/src/builtins/set/tests.rs +++ b/core/engine/src/builtins/set/tests.rs @@ -174,3 +174,190 @@ fn not_a_function() { "calling a builtin Set constructor without new is forbidden", )]); } + +#[test] +fn difference() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([1, 3, 5, 7, 9]); + let setB = new Set([1, 4, 9]); + "#}), + TestAction::assert_with_op("setA.difference(setB)", |v, _| { + v.display().to_string() == "Set { 3, 5, 7 }" + }), + TestAction::assert_with_op("setB.difference(setA)", |v, _| { + v.display().to_string() == "Set { 4 }" + }), + ]); +} + +#[test] +fn difference_equal_set() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([1, 3, 5, 7, 9]); + let setB = new Set([1, 4, 5, 7, 9]); + "#}), + TestAction::assert_with_op("setA.difference(setB)", |v, _| { + v.display().to_string() == "Set { 3 }" + }), + TestAction::assert_with_op("setB.difference(setA)", |v, _| { + v.display().to_string() == "Set { 4 }" + }), + ]); +} + +#[test] +fn difference_empty() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([1, 3, 5, 7, 9]); + let setB = new Set([]); + "#}), + TestAction::assert_with_op("setA.difference(setB)", |v, _| { + v.display().to_string() == "Set { 1, 3, 5, 7, 9 }" + }), + TestAction::assert_with_op("setB.difference(setA)", |v, _| { + v.display().to_string() == "Set(0)" + }), + ]); +} + +#[test] +fn intersection() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([1,2,3]); + let setB = new Set([1,4,3]); + let setC = new Set([]); + "#}), + TestAction::assert_with_op("setA.intersection(setB)", |v, _| { + v.display().to_string() == "Set { 1, 3 }" + }), + TestAction::assert_with_op("setB.intersection(setA)", |v, _| { + v.display().to_string() == "Set { 1, 3 }" + }), + TestAction::assert_with_op("setA.intersection(setA)", |v, _| { + v.display().to_string() == "Set { 1, 2, 3 }" + }), + TestAction::assert_with_op("setB.intersection(setB)", |v, _| { + v.display().to_string() == "Set { 1, 4, 3 }" + }), + TestAction::assert_with_op("setB.intersection(setC)", |v, _| { + v.display().to_string() == "Set { 1, 4, 3 }" + }), + TestAction::assert_with_op("setA.intersection(setC)", |v, _| { + v.display().to_string() == "Set { 1, 2, 3 }" + }), + ]); +} + +#[test] +fn is_dist_joint_from() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([1, 2, 3]); + let setB = new Set([1, 4, 6]); + let setC = new Set([4, 8, 15, 16 ,23 ,42]); + "#}), + TestAction::assert_with_op("setA.isDisjointFrom(setB)", |v, _| { + !v.as_boolean().unwrap_or(false) + }), + TestAction::assert_with_op("setA.isDisjointFrom(setC)", |v, _| { + v.as_boolean().unwrap_or(true) + }), + ]); +} + +#[test] +fn is_subset_of() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([4, 8, 15]); + let setB = new Set([1, 4, 6]); + let setC = new Set([4, 8, 15, 16 ,23 ,42]); + let setD = new Set([16]); + let setE = new Set([]); + "#}), + TestAction::assert_with_op("setA.isSubsetOf(setB)", |v, _| { + !v.as_boolean().unwrap_or(false) + }), + TestAction::assert_with_op("setA.isSubsetOf(setC)", |v, _| { + v.as_boolean().unwrap_or(true) + }), + TestAction::assert_with_op("setB.isSubsetOf(setC)", |v, _| { + !v.as_boolean().unwrap_or(false) + }), + TestAction::assert_with_op("setC.isSubsetOf(setC)", |v, _| { + v.as_boolean().unwrap_or(true) + }), + TestAction::assert_with_op("setD.isSubsetOf(setC)", |v, _| { + v.as_boolean().unwrap_or(true) + }), + TestAction::assert_with_op("setE.isSubsetOf(setC)", |v, _| { + v.as_boolean().unwrap_or(true) + }), + TestAction::assert_with_op("setA.isSubsetOf(setE)", |v, _| { + !v.as_boolean().unwrap_or(false) + }), + ]); +} + +#[test] +fn is_superset_of() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set(["JavaScript", "HTML", "CSS"]); + let setB = new Set(["HTML", "CSS"]); + "#}), + TestAction::assert_with_op("setA.isSupersetOf(setB)", |v, _| { + v.as_boolean().unwrap_or(false) + }), + TestAction::assert_with_op("setB.isSupersetOf(setA)", |v, _| { + !v.as_boolean().unwrap_or(false) + }), + ]); +} + +#[test] +fn symmetric_difference() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set(["JavaScript", "HTML", "CSS"]); + let setB = new Set(["Python", "Java", "JavaScript", "PHP"]); + let setC = new Set([2, 4, 6, 8]); + let setD = new Set([1, 4, 9]); + "#}), + TestAction::assert_with_op("setA.symmetricDifference(setB)", |v, _| { + v.display().to_string() == "Set { \"HTML\", \"CSS\", \"Python\", \"Java\", \"PHP\" }" + }), + TestAction::assert_with_op("setB.symmetricDifference(setA)", |v, _| { + v.display().to_string() == "Set { \"Python\", \"Java\", \"PHP\", \"HTML\", \"CSS\" }" + }), + TestAction::assert_with_op("setA.symmetricDifference(setA)", |v, _| { + v.display().to_string() == "Set(0)" + }), + TestAction::assert_with_op("setC.symmetricDifference(setD)", |v, _| { + v.display().to_string() == "Set { 2, 6, 8, 1, 9 }" + }), + TestAction::assert_with_op("setD.symmetricDifference(setC)", |v, _| { + v.display().to_string() == "Set { 1, 9, 2, 6, 8 }" + }), + ]); +} + +#[test] +fn union() { + run_test_actions([ + TestAction::run(indoc! {r#" + let setA = new Set([2, 4, 6, 8]); + let setB = new Set([1, 4, 9]); + "#}), + TestAction::assert_with_op("setA.union(setB)", |v, _| { + v.display().to_string() == "Set { 2, 4, 6, 8, 1, 9 }" + }), + TestAction::assert_with_op("setB.union(setA)", |v, _| { + v.display().to_string() == "Set { 1, 4, 9, 2, 6, 8 }" + }), + ]); +}