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}