Skip to main content

boa_engine/builtins/weak_map/
mod.rs

1//! Boa's implementation of ECMAScript's `WeakMap` builtin object.
2//!
3//! More information:
4//!  - [ECMAScript reference][spec]
5//!  - [MDN documentation][mdn]
6//!
7//! [spec]: https://tc39.es/ecma262/#sec-weakmap-objects
8//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
9
10use crate::{
11    Context, JsArgs, JsResult, JsString, JsValue,
12    builtins::{
13        BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
14        map::add_entries_from_iterable,
15    },
16    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
17    js_error, js_string,
18    object::{ErasedVTableObject, JsObject, internal_methods::get_prototype_from_constructor},
19    property::Attribute,
20    realm::Realm,
21    string::StaticJsStrings,
22    symbol::JsSymbol,
23};
24use boa_gc::{Finalize, Trace};
25
26pub(crate) type NativeWeakMap = boa_gc::WeakMap<ErasedVTableObject, JsValue>;
27
28#[derive(Debug, Trace, Finalize)]
29pub(crate) struct WeakMap;
30
31#[cfg(test)]
32mod tests;
33
34impl IntrinsicObject for WeakMap {
35    fn get(intrinsics: &Intrinsics) -> JsObject {
36        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
37    }
38
39    fn init(realm: &Realm) {
40        BuiltInBuilder::from_standard_constructor::<Self>(realm)
41            .property(
42                JsSymbol::to_string_tag(),
43                Self::NAME,
44                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
45            )
46            .method(Self::delete, js_string!("delete"), 1)
47            .method(Self::get, js_string!("get"), 1)
48            .method(Self::has, js_string!("has"), 1)
49            .method(Self::set, js_string!("set"), 2)
50            .method(Self::get_or_insert, js_string!("getOrInsert"), 2)
51            .method(
52                Self::get_or_insert_computed,
53                js_string!("getOrInsertComputed"),
54                2,
55            )
56            .build();
57    }
58}
59
60impl BuiltInObject for WeakMap {
61    const NAME: JsString = StaticJsStrings::WEAK_MAP;
62
63    const ATTRIBUTE: Attribute = Attribute::WRITABLE.union(Attribute::CONFIGURABLE);
64}
65
66impl BuiltInConstructor for WeakMap {
67    /// The amount of arguments the `WeakMap` constructor takes.
68    const CONSTRUCTOR_ARGUMENTS: usize = 0;
69    const PROTOTYPE_STORAGE_SLOTS: usize = 7;
70    const CONSTRUCTOR_STORAGE_SLOTS: usize = 0;
71
72    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
73        StandardConstructors::weak_map;
74
75    /// `WeakMap ( [ iterable ] )`
76    ///
77    /// More information:
78    ///  - [ECMAScript reference][spec]
79    ///  - [MDN documentation][mdn]
80    ///
81    /// [spec]: https://tc39.es/ecma262/#sec-weakmap-iterable
82    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/WeakMap
83    fn constructor(
84        new_target: &JsValue,
85        args: &[JsValue],
86        context: &mut Context,
87    ) -> JsResult<JsValue> {
88        // 1. If NewTarget is undefined, throw a TypeError exception.
89        if new_target.is_undefined() {
90            return Err(js_error!(TypeError: "WeakMap: cannot call constructor without `new`"));
91        }
92
93        // 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakMap.prototype%", « [[WeakMapData]] »).
94        // 3. Set map.[[WeakMapData]] to a new empty List.
95        let prototype =
96            get_prototype_from_constructor(new_target, StandardConstructors::weak_map, context)?;
97        let map = JsObject::from_proto_and_data_with_shared_shape(
98            context.root_shape(),
99            prototype,
100            NativeWeakMap::new(),
101        )
102        .upcast();
103
104        // 4. If iterable is either undefined or null, return map.
105        let iterable = args.get_or_undefined(0);
106        if iterable.is_null_or_undefined() {
107            return Ok(map.into());
108        }
109
110        // 5. Let adder be ? Get(map, "set").
111        // 6. If IsCallable(adder) is false, throw a TypeError exception.
112        let adder = map
113            .get(js_string!("set"), context)?
114            .as_function()
115            .ok_or_else(|| js_error!(TypeError: "WeakMap: 'add' is not a function"))?;
116
117        // 7. Return ? AddEntriesFromIterable(map, iterable, adder).
118        add_entries_from_iterable(&map, iterable, &adder, context)
119    }
120}
121
122impl WeakMap {
123    /// `WeakMap.prototype.delete ( key )`
124    ///
125    /// More information:
126    ///  - [ECMAScript reference][spec]
127    ///  - [MDN documentation][mdn]
128    ///
129    /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.delete
130    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/delete
131    pub(crate) fn delete(
132        this: &JsValue,
133        args: &[JsValue],
134        _context: &mut Context,
135    ) -> JsResult<JsValue> {
136        // 1. Let M be the this value.
137        // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
138        let object = this.as_object();
139        let mut map = object
140            .as_ref()
141            .and_then(JsObject::downcast_mut::<NativeWeakMap>)
142            .ok_or_else(|| {
143                js_error!(TypeError:
144                    "WeakMap.prototype.delete: expected 'this' to be a WeakMap object",
145                )
146            })?;
147
148        // 3. Let entries be M.[[WeakMapData]].
149        // 4. If key is not an Object, return false.
150        let Some(key) = args.get_or_undefined(0).as_object() else {
151            return Ok(false.into());
152        };
153
154        // 5. For each Record { [[Key]], [[Value]] } p of entries, do
155        // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
156        // i. Set p.[[Key]] to empty.
157        // ii. Set p.[[Value]] to empty.
158        // iii. Return true.
159        // 6. Return false.
160        Ok(map.remove(key.inner()).into())
161    }
162
163    /// `WeakMap.prototype.get ( key )`
164    ///
165    /// More information:
166    ///  - [ECMAScript reference][spec]
167    ///  - [MDN documentation][mdn]
168    ///
169    /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.get
170    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/get
171    pub(crate) fn get(
172        this: &JsValue,
173        args: &[JsValue],
174        _context: &mut Context,
175    ) -> JsResult<JsValue> {
176        // 1. Let M be the this value.
177        // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
178        let object = this.as_object();
179        let map = object
180            .as_ref()
181            .and_then(JsObject::downcast_ref::<NativeWeakMap>)
182            .ok_or_else(|| {
183                js_error!(TypeError:
184                    "WeakMap.prototype.get: expected 'this' to be a WeakMap object")
185            })?;
186
187        // 3. Let entries be M.[[WeakMapData]].
188        // 4. If key is not an Object, return undefined.
189        let Some(key) = args.get_or_undefined(0).as_object() else {
190            return Ok(JsValue::undefined());
191        };
192
193        // 5. For each Record { [[Key]], [[Value]] } p of entries, do
194        // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
195        // 6. Return undefined.
196        if let Some(entry) = map.get(key.inner())
197            && let Some(val) = entry.value()
198        {
199            Ok(val.clone())
200        } else {
201            Ok(JsValue::undefined())
202        }
203    }
204
205    /// `WeakMap.prototype.has ( key )`
206    ///
207    /// More information:
208    ///  - [ECMAScript reference][spec]
209    ///  - [MDN documentation][mdn]
210    ///
211    /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.has
212    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/has
213    pub(crate) fn has(
214        this: &JsValue,
215        args: &[JsValue],
216        _context: &mut Context,
217    ) -> JsResult<JsValue> {
218        // 1. Let M be the this value.
219        // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
220        let object = this.as_object();
221        let map = object
222            .as_ref()
223            .and_then(JsObject::downcast_ref::<NativeWeakMap>)
224            .ok_or_else(|| {
225                js_error!(TypeError:
226                    "WeakMap.prototype.has: expected 'this' to be a WeakMap object")
227            })?;
228
229        // 3. Let entries be M.[[WeakMapData]].
230        // 4. If key is not an Object, return false.
231        let Some(key) = args.get_or_undefined(0).as_object() else {
232            return Ok(false.into());
233        };
234
235        // 5. For each Record { [[Key]], [[Value]] } p of entries, do
236        // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return true.
237        // 6. Return false.
238        Ok(map.contains_key(key.inner()).into())
239    }
240
241    /// `WeakMap.prototype.set ( key, value )`
242    ///
243    /// More information:
244    ///  - [ECMAScript reference][spec]
245    ///  - [MDN documentation][mdn]
246    ///
247    /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.set
248    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/set
249    pub(crate) fn set(
250        this: &JsValue,
251        args: &[JsValue],
252        _context: &mut Context,
253    ) -> JsResult<JsValue> {
254        // 1. Let M be the this value.
255        // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
256        let object = this.as_object();
257        let mut map = object
258            .as_ref()
259            .and_then(JsObject::downcast_mut::<NativeWeakMap>)
260            .ok_or_else(|| {
261                js_error!(TypeError:
262                    "WeakMap.prototype.set: expected 'this' to be a WeakMap object")
263            })?;
264
265        // 3. Let entries be M.[[WeakMapData]].
266        // 4. If key is not an Object, throw a TypeError exception.
267        let key = args.get_or_undefined(0);
268        let Some(key) = key.as_object() else {
269            return Err(js_error!(TypeError:
270                "WeakMap.set: expected target argument of type `object`, got target of type `{}`",
271                key.type_of()
272            ));
273        };
274
275        // 5. For each Record { [[Key]], [[Value]] } p of entries, do
276        // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
277        // i. Set p.[[Value]] to value.
278        // ii. Return M.
279        // 6. Let p be the Record { [[Key]]: key, [[Value]]: value }.
280        // 7. Append p to entries.
281        map.insert(key.inner(), args.get_or_undefined(1).clone());
282
283        // 8. Return M.
284        Ok(this.clone())
285    }
286
287    /// `WeakMap.prototype.getOrInsert ( key, value )`
288    ///
289    /// Given a key and a value, returns the existing value if it exists; otherwise inserts the
290    /// provided default value and returns that value.
291    ///
292    /// More information:
293    ///  - [Upsert proposal reference][spec]
294    ///  - [MDN documentation][mdn]
295    ///
296    /// [spec]: https://tc39.es/proposal-upsert/#sec-weakmap.prototype.getOrInsert
297    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/getOrInsert
298    pub(crate) fn get_or_insert(
299        this: &JsValue,
300        args: &[JsValue],
301        _context: &mut Context,
302    ) -> JsResult<JsValue> {
303        // 1. Let M be the this value.
304        // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
305        let object = this.as_object();
306        let map = object
307            .and_then(|obj| obj.clone().downcast::<NativeWeakMap>().ok())
308            .ok_or_else(|| {
309                js_error!(TypeError:
310                    "WeakMap.prototype.getOrInsert: expected 'this' to be a WeakMap object",
311                )
312            })?;
313
314        // 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
315        // TODO: Implement proper CanBeHeldWeakly once available. For now, only
316        //       objects are accepted as keys; symbols should be allowed in the
317        //       future according to the proposal.
318        let key_val = args.get_or_undefined(0);
319        let Some(key) = key_val.as_object() else {
320            return Err(js_error!(TypeError:
321                "WeakMap.getOrInsert: expected target argument of type `object`, got target of type `{}`",
322                key_val.type_of()
323            ));
324        };
325
326        // 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]]
327        if let Some(existing) = map.borrow().data().get(key.inner())
328            && let Some(value) = existing.value()
329        {
330            // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
331            return Ok(value.clone());
332        }
333
334        // 5-6. Insert the new record with provided value and return it.
335        let value = args.get_or_undefined(1).clone();
336        map.borrow_mut()
337            .data_mut()
338            .insert(key.inner(), value.clone());
339        Ok(value)
340    }
341
342    /// `WeakMap.prototype.getOrInsertComputed ( key, callback )`
343    ///
344    /// If the key exists, returns the existing value. Otherwise computes a new value by calling
345    /// `callback` with the key, inserts it into the `WeakMap`, and returns it.
346    ///
347    /// More information:
348    ///  - [Upsert proposal reference][spec]
349    ///  - [MDN documentation][mdn]
350    ///
351    /// [spec]: https://tc39.es/proposal-upsert/#sec-weakmap.prototype.getOrInsertComputed
352    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/getOrInsertComputed
353    pub(crate) fn get_or_insert_computed(
354        this: &JsValue,
355        args: &[JsValue],
356        context: &mut Context,
357    ) -> JsResult<JsValue> {
358        // 1. Let M be the this value.
359        // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
360        let object = this.as_object();
361        let map = object
362            .and_then(|obj| obj.clone().downcast::<NativeWeakMap>().ok())
363            .ok_or_else(|| {
364                js_error!(TypeError:
365                    "WeakMap.prototype.getOrInsertComputed: expected 'this' to be a WeakMap object",
366                )
367            })?;
368
369        // 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
370        // TODO: Implement proper CanBeHeldWeakly once available. For now, only
371        //       objects are accepted as keys; symbols should be allowed in the
372        //       future according to the proposal.
373        let key_value = args.get_or_undefined(0).clone();
374        let Some(key_obj) = key_value.as_object() else {
375            return Err(js_error!(TypeError:
376                "WeakMap.getOrInsertComputed: expected target argument of type `object`, got target of type `{}`",
377                key_value.type_of()
378            ));
379        };
380
381        // 4. If IsCallable(callback) is false, throw a TypeError exception.
382        let Some(callback_fn) = args.get_or_undefined(1).as_callable() else {
383            return Err(js_error!(TypeError:
384                "Method WeakMap.prototype.getOrInsertComputed called with non-callable callback function"
385            ));
386        };
387
388        // 5. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]]
389        if let Some(existing) = map.borrow().data().get(key_obj.inner())
390            && let Some(value) = existing.value()
391        {
392            // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
393            return Ok(value.clone());
394        }
395
396        // 6. Let value be ? Call(callback, undefined, « key »).
397        // 7. NOTE: The WeakMap may have been modified during execution of callback.
398        let value = callback_fn.call(
399            &JsValue::undefined(),
400            std::slice::from_ref(&key_value),
401            context,
402        )?;
403
404        // 8-10. Insert or update the entry and return value.
405        map.borrow_mut()
406            .data_mut()
407            .insert(key_obj.inner(), value.clone());
408        Ok(value)
409    }
410}