Skip to main content

boa_engine/builtins/weak_set/
mod.rs

1//! Boa's implementation of ECMAScript's `WeakSet` builtin object.
2//!
3//! More information:
4//!  - [ECMAScript reference][spec]
5//!  - [MDN documentation][mdn]
6//!
7//! [spec]: https://tc39.es/ecma262/#sec-weakset-objects
8//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet
9
10use crate::{
11    Context, JsArgs, JsResult, JsString, JsValue,
12    builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
13    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
14    js_error, js_string,
15    object::{ErasedVTableObject, JsObject, internal_methods::get_prototype_from_constructor},
16    property::Attribute,
17    realm::Realm,
18    string::StaticJsStrings,
19    symbol::JsSymbol,
20};
21use boa_gc::{Finalize, Trace};
22
23use super::iterable::IteratorHint;
24
25pub(crate) type NativeWeakSet = boa_gc::WeakMap<ErasedVTableObject, ()>;
26
27#[derive(Debug, Trace, Finalize)]
28pub(crate) struct WeakSet;
29
30impl IntrinsicObject for WeakSet {
31    fn get(intrinsics: &Intrinsics) -> JsObject {
32        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
33    }
34
35    fn init(realm: &Realm) {
36        BuiltInBuilder::from_standard_constructor::<Self>(realm)
37            .property(
38                JsSymbol::to_string_tag(),
39                Self::NAME,
40                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
41            )
42            .method(Self::add, js_string!("add"), 1)
43            .method(Self::delete, js_string!("delete"), 1)
44            .method(Self::has, js_string!("has"), 1)
45            .build();
46    }
47}
48
49impl BuiltInObject for WeakSet {
50    const NAME: JsString = StaticJsStrings::WEAK_SET;
51
52    const ATTRIBUTE: Attribute = Attribute::WRITABLE.union(Attribute::CONFIGURABLE);
53}
54
55impl BuiltInConstructor for WeakSet {
56    /// The amount of arguments the `WeakSet` constructor takes.
57    const CONSTRUCTOR_ARGUMENTS: usize = 0;
58    const PROTOTYPE_STORAGE_SLOTS: usize = 4;
59    const CONSTRUCTOR_STORAGE_SLOTS: usize = 0;
60
61    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
62        StandardConstructors::weak_set;
63
64    /// `WeakSet ( [ iterable ] )`
65    ///
66    /// More information:
67    ///  - [ECMAScript reference][spec]
68    ///  - [MDN documentation][mdn]
69    ///
70    /// [spec]: https://tc39.es/ecma262/#sec-weakset-iterable
71    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/WeakSet
72    fn constructor(
73        new_target: &JsValue,
74        args: &[JsValue],
75        context: &mut Context,
76    ) -> JsResult<JsValue> {
77        // 1. If NewTarget is undefined, throw a TypeError exception.
78        if new_target.is_undefined() {
79            return Err(js_error!(TypeError: "WeakSet: cannot call constructor without `new`"));
80        }
81
82        // 2. Let set be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakSet.prototype%", « [[WeakSetData]] »).
83        // 3. Set set.[[WeakSetData]] to a new empty List.
84        let prototype =
85            get_prototype_from_constructor(new_target, StandardConstructors::weak_set, context)?;
86        let weak_set = JsObject::from_proto_and_data_with_shared_shape(
87            context.root_shape(),
88            prototype,
89            NativeWeakSet::new(),
90        )
91        .upcast();
92
93        // 4. If iterable is either undefined or null, return set.
94        let iterable = args.get_or_undefined(0);
95        if iterable.is_null_or_undefined() {
96            return Ok(weak_set.into());
97        }
98
99        // 5. Let adder be ? Get(set, "add").
100        let adder = weak_set.get(js_string!("add"), context)?;
101
102        // 6. If IsCallable(adder) is false, throw a TypeError exception.
103        let adder = adder
104            .as_callable()
105            .ok_or_else(|| js_error!(TypeError: "WeakSet: 'add' is not a function"))?;
106
107        // 7. Let iteratorRecord be ? GetIterator(iterable, sync).
108        let mut iterator_record = iterable.clone().get_iterator(IteratorHint::Sync, context)?;
109
110        // 8. Repeat,
111        //     a. Let next be ? IteratorStepValue(iteratorRecord).
112        while let Some(next) = iterator_record.step_value(context)? {
113            //     c. Let status be Completion(Call(adder, set, « next »)).
114            if let Err(status) = adder.call(&weak_set.clone().into(), &[next], context) {
115                //     d. IfAbruptCloseIterator(status, iteratorRecord).
116                return iterator_record.close(Err(status), context);
117            }
118        }
119
120        //     b. If next is done, return set.
121        Ok(weak_set.into())
122    }
123}
124
125impl WeakSet {
126    /// `WeakSet.prototype.add( value )`
127    ///
128    /// The `add()` method appends a new object to the end of a `WeakSet` object.
129    ///
130    /// More information:
131    ///  - [ECMAScript reference][spec]
132    ///  - [MDN documentation][mdn]
133    ///
134    /// [spec]: https://tc39.es/ecma262/#sec-weakset.prototype.add
135    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/add
136    pub(crate) fn add(
137        this: &JsValue,
138        args: &[JsValue],
139        _context: &mut Context,
140    ) -> JsResult<JsValue> {
141        // 1. Let S be the this value.
142        // 2. Perform ? RequireInternalSlot(S, [[WeakSetData]]).
143        let object = this.as_object();
144        let mut set = object
145            .as_ref()
146            .and_then(JsObject::downcast_mut::<NativeWeakSet>)
147            .ok_or_else(|| {
148                js_error!(TypeError:
149                    "WeakSet.prototype.add: expected 'this' to be a WeakSet object")
150            })?;
151
152        // 3. If Type(value) is not Object, throw a TypeError exception.
153        let value = args.get_or_undefined(0);
154        let Some(value) = value.as_object() else {
155            return Err(js_error!(TypeError:
156                "WeakSet.add: expected target argument of type `object`, got target of type `{}`",
157                value.type_of()
158            ));
159        };
160
161        // 4. Let entries be the List that is S.[[WeakSetData]].
162        // 5. For each element e of entries, do
163        if set.contains_key(value.inner()) {
164            // a. If e is not empty and SameValue(e, value) is true, then
165            // i. Return S.
166            return Ok(this.clone());
167        }
168
169        // 6. Append value as the last element of entries.
170        set.insert(value.inner(), ());
171
172        // 7. Return S.
173        Ok(this.clone())
174    }
175
176    /// `WeakSet.prototype.delete( value )`
177    ///
178    /// The `delete()` method removes the specified element from a `WeakSet` object.
179    ///
180    /// More information:
181    ///  - [ECMAScript reference][spec]
182    ///  - [MDN documentation][mdn]
183    ///
184    /// [spec]: https://tc39.es/ecma262/#sec-weakset.prototype.delete
185    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/delete
186    pub(crate) fn delete(
187        this: &JsValue,
188        args: &[JsValue],
189        _context: &mut Context,
190    ) -> JsResult<JsValue> {
191        // 1. Let S be the this value.
192        // 2. Perform ? RequireInternalSlot(S, [[WeakSetData]]).
193        let object = this.as_object();
194        let mut set = object
195            .as_ref()
196            .and_then(JsObject::downcast_mut::<NativeWeakSet>)
197            .ok_or_else(|| {
198                js_error!(TypeError:
199                    "WeakSet.prototype.delete: expected 'this' to be a WeakSet object",
200                )
201            })?;
202
203        // 3. If Type(value) is not Object, return false.
204        let value = args.get_or_undefined(0);
205        let Some(value) = value.as_object() else {
206            return Ok(false.into());
207        };
208
209        // 4. Let entries be the List that is S.[[WeakSetData]].
210        // 5. For each element e of entries, do
211        // a. If e is not empty and SameValue(e, value) is true, then
212        // i. Replace the element of entries whose value is e with an element whose value is empty.
213        // ii. Return true.
214        // 6. Return false.
215        Ok(set.remove(value.inner()).into())
216    }
217
218    /// `WeakSet.prototype.has( value )`
219    ///
220    /// The `has()` method returns a boolean indicating whether an object exists in a `WeakSet` or not.
221    ///
222    /// More information:
223    ///  - [ECMAScript reference][spec]
224    ///  - [MDN documentation][mdn]
225    ///
226    /// [spec]: https://tc39.es/ecma262/#sec-weakset.prototype.has
227    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/has
228    pub(crate) fn has(
229        this: &JsValue,
230        args: &[JsValue],
231        _context: &mut Context,
232    ) -> JsResult<JsValue> {
233        // 1. Let S be the this value.
234        // 2. Perform ? RequireInternalSlot(S, [[WeakSetData]]).
235        let object = this.as_object();
236        let set = object
237            .as_ref()
238            .and_then(JsObject::downcast_ref::<NativeWeakSet>)
239            .ok_or_else(|| {
240                js_error!(TypeError:
241                    "WeakSet.prototype.has: expected 'this' to be a WeakSet object")
242            })?;
243
244        // 3. Let entries be the List that is S.[[WeakSetData]].
245        // 4. If Type(value) is not Object, return false.
246        let value = args.get_or_undefined(0);
247        let Some(value) = value.as_object() else {
248            return Ok(false.into());
249        };
250
251        // 5. For each element e of entries, do
252        // a. If e is not empty and SameValue(e, value) is true, return true.
253        // 6. Return false.
254        Ok(set.contains_key(value.inner()).into())
255    }
256}
257
258#[cfg(test)]
259mod tests;