boa_engine/builtins/map/mod.rs
1//! Boa's implementation of ECMAScript's global `Map` object.
2//!
3//! The ECMAScript `Map` class is a global object that is used in the construction of maps; which
4//! are high-level, key-value stores.
5//!
6//! More information:
7//! - [ECMAScript reference][spec]
8//! - [MDN documentation][mdn]
9//!
10//! [spec]: https://tc39.es/ecma262/#sec-map-objects
11//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
12
13use crate::{
14 Context, JsArgs, JsResult, JsString, JsValue,
15 builtins::{BuiltInObject, iterable::IteratorHint, map::ordered_map::MapLock},
16 context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
17 error::JsNativeError,
18 js_error, js_string,
19 object::{JsFunction, JsObject, internal_methods::get_prototype_from_constructor},
20 property::{Attribute, PropertyNameKind},
21 realm::Realm,
22 string::StaticJsStrings,
23 symbol::JsSymbol,
24};
25
26use super::{
27 BuiltInBuilder, BuiltInConstructor, IntrinsicObject, canonicalize_keyed_collection_key,
28 iterable::if_abrupt_close_iterator,
29};
30
31mod map_iterator;
32pub(crate) use map_iterator::MapIterator;
33
34pub mod ordered_map;
35use ordered_map::OrderedMap;
36
37#[cfg(test)]
38mod tests;
39
40#[derive(Debug, Clone)]
41pub(crate) struct Map;
42
43impl IntrinsicObject for Map {
44 fn init(realm: &Realm) {
45 let get_species = BuiltInBuilder::callable(realm, Self::get_species)
46 .name(js_string!("get [Symbol.species]"))
47 .build();
48
49 let get_size = BuiltInBuilder::callable(realm, Self::get_size)
50 .name(js_string!("get size"))
51 .build();
52
53 let entries_function = BuiltInBuilder::callable(realm, Self::entries)
54 .name(js_string!("entries"))
55 .build();
56
57 BuiltInBuilder::from_standard_constructor::<Self>(realm)
58 .static_method(Self::group_by, js_string!("groupBy"), 2)
59 .static_accessor(
60 JsSymbol::species(),
61 Some(get_species),
62 None,
63 Attribute::CONFIGURABLE,
64 )
65 .property(
66 js_string!("entries"),
67 entries_function.clone(),
68 Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
69 )
70 .property(
71 JsSymbol::iterator(),
72 entries_function,
73 Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
74 )
75 .property(
76 JsSymbol::to_string_tag(),
77 Self::NAME,
78 Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
79 )
80 .method(Self::clear, js_string!("clear"), 0)
81 .method(Self::delete, js_string!("delete"), 1)
82 .method(Self::for_each, js_string!("forEach"), 1)
83 .method(Self::get, js_string!("get"), 1)
84 .method(Self::has, js_string!("has"), 1)
85 .method(Self::keys, js_string!("keys"), 0)
86 .method(Self::set, js_string!("set"), 2)
87 .method(Self::values, js_string!("values"), 0)
88 .method(Self::get_or_insert, js_string!("getOrInsert"), 2)
89 .method(
90 Self::get_or_insert_computed,
91 js_string!("getOrInsertComputed"),
92 2,
93 )
94 .accessor(
95 js_string!("size"),
96 Some(get_size),
97 None,
98 Attribute::CONFIGURABLE,
99 )
100 .build();
101 }
102
103 fn get(intrinsics: &Intrinsics) -> JsObject {
104 Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
105 }
106}
107
108impl BuiltInObject for Map {
109 const NAME: JsString = StaticJsStrings::MAP;
110}
111
112impl BuiltInConstructor for Map {
113 const CONSTRUCTOR_ARGUMENTS: usize = 0;
114 const PROTOTYPE_STORAGE_SLOTS: usize = 15;
115 const CONSTRUCTOR_STORAGE_SLOTS: usize = 3;
116
117 const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
118 StandardConstructors::map;
119
120 /// `Map ( [ iterable ] )`
121 ///
122 /// Constructor for `Map` objects.
123 ///
124 /// More information:
125 /// - [ECMAScript reference][spec]
126 /// - [MDN documentation][mdn]
127 ///
128 /// [spec]: https://tc39.es/ecma262/#sec-map-iterable
129 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map
130 fn constructor(
131 new_target: &JsValue,
132 args: &[JsValue],
133 context: &mut Context,
134 ) -> JsResult<JsValue> {
135 // 1. If NewTarget is undefined, throw a TypeError exception.
136 if new_target.is_undefined() {
137 return Err(js_error!(
138 TypeError: "cannot call `Map` constructor without new"
139 ));
140 }
141
142 // 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%Map.prototype%", « [[MapData]] »).
143 // 3. Set map.[[MapData]] to a new empty List.
144 let prototype =
145 get_prototype_from_constructor(new_target, StandardConstructors::map, context)?;
146 let map = JsObject::from_proto_and_data_with_shared_shape(
147 context.root_shape(),
148 prototype,
149 <OrderedMap<JsValue>>::new(),
150 )
151 .upcast();
152
153 // 4. If iterable is either undefined or null, return map.
154 let iterable = match args.get_or_undefined(0) {
155 val if !val.is_null_or_undefined() => val,
156 _ => return Ok(map.into()),
157 };
158
159 // 5. Let adder be ? Get(map, "set").
160 // 6. If IsCallable(adder) is false, throw a TypeError exception.
161 let adder = map
162 .get(js_string!("set"), context)?
163 .as_function()
164 .ok_or_else(
165 || js_error!(TypeError: "constructor `Map` cannot use non-callable adder"),
166 )?;
167
168 // 7. Return ? AddEntriesFromIterable(map, iterable, adder).
169 add_entries_from_iterable(&map, iterable, &adder, context)
170 }
171}
172
173impl Map {
174 /// `get Map [ @@species ]`
175 ///
176 /// The `Map [ @@species ]` accessor property returns the Map constructor.
177 ///
178 /// More information:
179 /// - [ECMAScript reference][spec]
180 /// - [MDN documentation][mdn]
181 ///
182 /// [spec]: https://tc39.es/ecma262/#sec-get-map-@@species
183 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species
184 #[allow(clippy::unnecessary_wraps)]
185 fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
186 // 1. Return the this value.
187 Ok(this.clone())
188 }
189
190 /// `Map.prototype.entries()`
191 ///
192 /// Returns a new Iterator object that contains the [key, value] pairs for each element in the Map object in insertion order.
193 ///
194 /// More information:
195 /// - [ECMAScript reference][spec]
196 /// - [MDN documentation][mdn]
197 ///
198 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.entries
199 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
200 pub(crate) fn entries(
201 this: &JsValue,
202 _: &[JsValue],
203 context: &mut Context,
204 ) -> JsResult<JsValue> {
205 // 1. Let M be the this value.
206 let this = this
207 .as_object()
208 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
209 .ok_or_else(|| {
210 js_error!(
211 TypeError: "method `Map.prototype.entries` called on incompatible receiver"
212 )
213 })?;
214
215 // 2. Return ? CreateMapIterator(M, key+value).
216 Ok(MapIterator::create_map_iterator(
217 this,
218 PropertyNameKind::KeyAndValue,
219 context,
220 ))
221 }
222
223 /// `Map.prototype.keys()`
224 ///
225 /// Returns a new Iterator object that contains the keys for each element in the Map object in insertion order.
226 ///
227 /// More information:
228 /// - [ECMAScript reference][spec]
229 /// - [MDN documentation][mdn]
230 ///
231 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.keys
232 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
233 pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
234 // 1. Let M be the this value.
235 let this = this
236 .as_object()
237 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
238 .ok_or_else(|| js_error!(TypeError: "method `Map.prototype.keys` called on incompatible receiver"))?;
239
240 // 2. Return ? CreateMapIterator(M, key).
241 Ok(MapIterator::create_map_iterator(
242 this,
243 PropertyNameKind::Key,
244 context,
245 ))
246 }
247
248 /// `Map.prototype.set( key, value )`
249 ///
250 /// Inserts a new entry in the Map object.
251 ///
252 /// More information:
253 /// - [ECMAScript reference][spec]
254 /// - [MDN documentation][mdn]
255 ///
256 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.set
257 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set
258 pub(crate) fn set(this: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
259 let value = args.get_or_undefined(1);
260
261 // 1. Let M be the this value.
262 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
263 let map = this
264 .as_object()
265 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
266 .ok_or_else(|| {
267 js_error!(TypeError: "method `Map.prototype.set` called on incompatible receiver")
268 })?;
269
270 // 3. Set key to CanonicalizeKeyedCollectionKey(key).
271 let key = canonicalize_keyed_collection_key(args.get_or_undefined(0).clone());
272
273 // 4. For each Record { [[Key]], [[Value]] } p of entries, do
274 // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then
275 // i. Set p.[[Value]] to value.
276 // 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
277 // 6. Append p to M.[[MapData]].
278 map.borrow_mut().data_mut().insert(key, value.clone());
279
280 // 7. Return M.
281 Ok(this.clone())
282 }
283
284 /// `get Map.prototype.size`
285 ///
286 /// Obtains the size of the map, filtering empty keys to ensure it updates
287 /// while iterating.
288 ///
289 /// More information:
290 /// - [ECMAScript reference][spec]
291 /// - [MDN documentation][mdn]
292 ///
293 /// [spec]: https://tc39.es/ecma262/#sec-get-map.prototype.size
294 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size
295 pub(crate) fn get_size(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
296 // 1. Let M be the this value.
297 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
298 // 3. Let entries be the List that is M.[[MapData]].
299 // 4. Let count be 0.
300 // 5. For each Record { [[Key]], [[Value]] } p of entries, do
301 // a. If p.[[Key]] is not empty, set count to count + 1.
302 // 6. Return 𝔽(count).
303 Ok(this
304 .as_object()
305 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
306 .ok_or_else(|| {
307 js_error!(TypeError: "method `Map.prototype.set` called on incompatible receiver")
308 })?
309 .borrow()
310 .data()
311 .len()
312 .into())
313 }
314
315 /// `Map.prototype.delete( key )`
316 ///
317 /// Removes the element associated with the key, if it exists.
318 /// Returns true if there was an element, and false otherwise.
319 ///
320 /// More information:
321 /// - [ECMAScript reference][spec]
322 /// - [MDN documentation][mdn]
323 ///
324 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.delete
325 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete
326 pub(crate) fn delete(this: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
327 // 1. Let M be the this value.
328 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
329 let map = this
330 .as_object()
331 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
332 .ok_or_else(|| {
333 js_error!(TypeError: "method `Map.prototype.delete` called on incompatible receiver")
334 })?;
335
336 // 3. Set key to CanonicalizeKeyedCollectionKey(key).
337 let key = canonicalize_keyed_collection_key(args.get_or_undefined(0).clone());
338
339 // 4. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
340 // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
341 // i. Set p.[[Key]] to empty.
342 // ii. Set p.[[Value]] to empty.
343 // iii. Return true.
344 // 5. Return false.
345 Ok(map.borrow_mut().data_mut().remove(&key).is_some().into())
346 }
347
348 /// `Map.prototype.get( key )`
349 ///
350 /// Returns the value associated with the key, or undefined if there is none.
351 ///
352 /// More information:
353 /// - [ECMAScript reference][spec]
354 /// - [MDN documentation][mdn]
355 ///
356 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.get
357 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
358 pub(crate) fn get(this: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
359 // 1. Let M be the this value.
360 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
361 let map = this
362 .as_object()
363 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
364 .ok_or_else(|| {
365 js_error!(TypeError: "method `Map.prototype.get` called on incompatible receiver")
366 })?;
367
368 // 3. Set key to CanonicalizeKeyedCollectionKey(key).
369 let key = canonicalize_keyed_collection_key(args.get_or_undefined(0).clone());
370
371 // 4. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
372 // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
373 // 5. Return undefined.
374 Ok(map.borrow().data().get(&key).cloned().unwrap_or_default())
375 }
376
377 /// `Map.prototype.clear( )`
378 ///
379 /// Removes all entries from the map.
380 ///
381 /// More information:
382 /// - [ECMAScript reference][spec]
383 /// - [MDN documentation][mdn]
384 ///
385 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.clear
386 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear
387 pub(crate) fn clear(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
388 // 1. Let M be the this value.
389 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
390 let map = this
391 .as_object()
392 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
393 .ok_or_else(|| {
394 js_error!(
395 TypeError: "method `Map.prototype.clear` called on incompatible receiver"
396 )
397 })?;
398
399 // 3. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
400 // a. Set p.[[Key]] to empty.
401 // b. Set p.[[Value]] to empty.
402 map.borrow_mut().data_mut().clear();
403
404 // 4. Return undefined.
405 Ok(JsValue::undefined())
406 }
407
408 /// `Map.prototype.has( key )`
409 ///
410 /// Checks if the map contains an entry with the given key.
411 ///
412 /// More information:
413 /// - [ECMAScript reference][spec]
414 /// - [MDN documentation][mdn]
415 ///
416 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has
417 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
418 pub(crate) fn has(this: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
419 // 1. Let M be the this value.
420 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
421 let map = this
422 .as_object()
423 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
424 .ok_or_else(|| {
425 js_error!(
426 TypeError: "method `Map.prototype.has` called on incompatible receiver"
427 )
428 })?;
429
430 // 3. Set key to CanonicalizeKeyedCollectionKey(key).
431 let key = canonicalize_keyed_collection_key(args.get_or_undefined(0).clone());
432
433 // 4. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
434 // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return true.
435 // 5. Return false.
436 Ok(map.borrow().data().contains_key(&key).into())
437 }
438
439 /// `Map.prototype.forEach( callbackFn [ , thisArg ] )`
440 ///
441 /// Executes the provided callback function for each key-value pair in the map.
442 ///
443 /// More information:
444 /// - [ECMAScript reference][spec]
445 /// - [MDN documentation][mdn]
446 ///
447 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.foreach
448 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
449 pub(crate) fn for_each(
450 this: &JsValue,
451 args: &[JsValue],
452 context: &mut Context,
453 ) -> JsResult<JsValue> {
454 // 1. Let M be the this value.
455 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
456 let map = this
457 .as_object()
458 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
459 .ok_or_else(|| {
460 js_error!(
461 TypeError: "method `Map.prototype.forEach` called on incompatible receiver"
462 )
463 })?;
464
465 // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
466 let Some(callback) = args.get_or_undefined(0).as_callable() else {
467 return Err(js_error!(
468 TypeError:
469 "Method Map.prototype.forEach called with non-callable callback function"
470 ));
471 };
472
473 let this_arg = args.get_or_undefined(1);
474
475 // NOTE:
476 //
477 // forEach does not directly mutate the object on which it is called but
478 // the object may be mutated by the calls to callbackfn. Each entry of a
479 // map's [[MapData]] is only visited once. New keys added after the call
480 // to forEach begins are visited. A key will be revisited if it is deleted
481 // after it has been visited and then re-added before the forEach call completes.
482 // Keys that are deleted after the call to forEach begins and before being visited
483 // are not visited unless the key is added again before the forEach call completes.
484 let _lock = MapLock::new(&map);
485
486 // 4. Let entries be the List that is M.[[MapData]].
487 // 5. For each Record { [[Key]], [[Value]] } e of entries, do
488 let mut index = 0;
489 loop {
490 let arguments = {
491 let map = map.borrow();
492 let map = map.data();
493 if index < map.full_len() {
494 map.get_index(index)
495 .map(|(k, v)| [v.clone(), k.clone(), this.clone()])
496 } else {
497 // 6. Return undefined.
498 return Ok(JsValue::undefined());
499 }
500 };
501
502 // a. If e.[[Key]] is not empty, then
503 if let Some(arguments) = arguments {
504 // i. Perform ? Call(callbackfn, thisArg, « e.[[Value]], e.[[Key]], M »).
505 callback.call(this_arg, &arguments, context)?;
506 }
507
508 index += 1;
509 }
510 }
511
512 /// Call `f` for each `(key, value)` in the `Map`.
513 ///
514 /// Can not be used in [`Self::for_each`] because in that case will be
515 /// incorrect order for next steps of the algo:
516 /// ```txt
517 /// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
518 /// 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
519 /// ```
520 pub(crate) fn for_each_native<F>(this: &JsValue, mut f: F) -> JsResult<()>
521 where
522 F: FnMut(JsValue, JsValue) -> JsResult<()>,
523 {
524 // See `Self::for_each` for comments on the algo.
525
526 let map = this
527 .as_object()
528 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
529 .ok_or_else(|| {
530 js_error!(
531 TypeError: "method `Map.prototype.forEach` called on incompatible receiver"
532 )
533 })?;
534
535 let _lock = MapLock::new(&map);
536
537 let mut index = 0;
538 loop {
539 let (k, v) = {
540 let map = map.borrow();
541 let map = map.data();
542 if index >= map.full_len() {
543 return Ok(());
544 }
545
546 let Some((k, v)) = map.get_index(index) else {
547 continue;
548 };
549
550 (k.clone(), v.clone())
551 };
552
553 f(k, v)?;
554 index += 1;
555 }
556 }
557
558 /// `Map.prototype.values()`
559 ///
560 /// Returns a new Iterator object that contains the values for each element in the Map object in insertion order.
561 ///
562 /// More information:
563 /// - [ECMAScript reference][spec]
564 /// - [MDN documentation][mdn]
565 ///
566 /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.values
567 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
568 pub(crate) fn values(
569 this: &JsValue,
570 _: &[JsValue],
571 context: &mut Context,
572 ) -> JsResult<JsValue> {
573 // 1. Let M be the this value.
574 // 2. Return ? CreateMapIterator(M, value).
575 let this = this
576 .as_object()
577 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
578 .ok_or_else(|| {
579 js_error!(
580 TypeError: "method `Map.prototype.values` called on incompatible receiver"
581 )
582 })?;
583
584 Ok(MapIterator::create_map_iterator(
585 this,
586 PropertyNameKind::Value,
587 context,
588 ))
589 }
590
591 /// `Map.prototype.getOrInsert(key, value)`
592 ///
593 /// Given a key and a value, the getOrInsert method will return the existing value if it exists.
594 /// Otherwise insert the provided default value and return that value.
595 ///
596 /// More information:
597 /// - [Upsert proposal reference][spec]
598 /// - [MDN documentation][mdn]
599 ///
600 /// [spec]: https://tc39.es/proposal-upsert/#sec-map.prototype.getOrInsert
601 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/getOrInsert
602 pub(crate) fn get_or_insert(
603 this: &JsValue,
604 args: &[JsValue],
605 _: &mut Context,
606 ) -> JsResult<JsValue> {
607 let value = args.get_or_undefined(1);
608
609 // 1. Let M be the this value.
610 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
611 let map = this
612 .as_object()
613 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
614 .ok_or_else(|| {
615 js_error!(
616 TypeError: "method `Map.prototype.getOrInsert` called on incompatible receiver"
617 )
618 })?;
619
620 // 3. Set key to CanonicalizeKeyedCollectionKey(key).
621 let key = canonicalize_keyed_collection_key(args.get_or_undefined(0).clone());
622
623 // 4. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
624 if let Some(existing) = map.borrow().data().get(&key) {
625 // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
626 return Ok(existing.clone());
627 }
628
629 // 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
630 // 6. Append p to M.[[MapData]].
631 map.borrow_mut().data_mut().insert(key, value.clone());
632
633 // 7. Return value.
634 Ok(value.clone())
635 }
636
637 /// `Map.prototype.getOrInsertComputed(key, callback)`
638 ///
639 /// If the key exists in the Map, returns the existing value.
640 /// Otherwise computes a new value by calling `callback` with the key,
641 /// inserts it into the Map, and returns it.
642 ///
643 /// More information:
644 /// - [Upsert proposal reference][spec]
645 /// - [MDN documentation][mdn]
646 ///
647 /// [spec]: https://tc39.es/proposal-upsert/#sec-map.prototype.getOrInsertComputed
648 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/getOrInsertComputed
649 pub(crate) fn get_or_insert_computed(
650 this: &JsValue,
651 args: &[JsValue],
652 context: &mut Context,
653 ) -> JsResult<JsValue> {
654 // 1. Let M be the this value.
655 // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
656 let map = this
657 .as_object()
658 .and_then(|o| o.downcast::<OrderedMap<JsValue>>().ok())
659 .ok_or_else(|| {
660 js_error!(
661 TypeError: "method `Map.prototype.getOrInsertComputed` called on incompatible receiver"
662 )
663 })?;
664
665 // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
666 let Some(callback_fn) = args.get_or_undefined(1).as_callable() else {
667 return Err(js_error!(
668 TypeError: "method `Map.prototype.getOrInsertComputed` called with non-callable callback function"
669 ));
670 };
671
672 // 4. Set key to CanonicalizeKeyedCollectionKey(key).
673 let key = canonicalize_keyed_collection_key(args.get_or_undefined(0).clone());
674
675 // 5. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
676 if let Some(existing) = map.borrow().data().get(&key) {
677 // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
678 return Ok(existing.clone());
679 }
680
681 // 6. Let value be ? Call(callback, undefined, « key »).
682 // 7. NOTE: The Map may have been modified during execution of callback.
683 let value = callback_fn.call(&JsValue::undefined(), std::slice::from_ref(&key), context)?;
684
685 // 8. For each Record { [[Key]], [[Value]] } p of M.[[MapData]], do
686 // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
687 // i. Set p.[[Value]] to value.
688 // ii. Return value.
689 // 9. Let p be the Record { [[Key]]: key, [[Value]]: value }.
690 // 10. Append p to M.[[MapData]].
691 // [`OrderedMap::insert`] handles both cases
692 map.borrow_mut()
693 .data_mut()
694 .insert(key.clone(), value.clone());
695 // 11. Return value.
696 Ok(value)
697 }
698
699 /// [`Map.groupBy ( items, callbackfn )`][spec]
700 ///
701 /// [spec]: https://tc39.es/ecma262/#sec-map.groupby
702 pub(crate) fn group_by(
703 _: &JsValue,
704 args: &[JsValue],
705 context: &mut Context,
706 ) -> JsResult<JsValue> {
707 use std::hash::BuildHasherDefault;
708
709 use indexmap::IndexMap;
710 use rustc_hash::FxHasher;
711
712 use crate::builtins::{Array, Number, iterable::if_abrupt_close_iterator};
713
714 let items = args.get_or_undefined(0);
715 let callback = args.get_or_undefined(1);
716 // 1. Let groups be ? GroupBy(items, callbackfn, zero).
717
718 // `GroupBy`
719 // https://tc39.es/ecma262/#sec-groupby
720 // inlined to change the key type.
721
722 // 1. Perform ? RequireObjectCoercible(items).
723 items.require_object_coercible()?;
724
725 // 2. If IsCallable(callbackfn) is false, throw a TypeError exception.
726 let callback = callback.as_callable().ok_or_else(|| {
727 js_error!(
728 TypeError:
729 "method `Map.prototype.groupBy` called with non-callable callback function"
730 )
731 })?;
732
733 // 3. Let groups be a new empty List.
734 let mut groups: IndexMap<JsValue, Vec<JsValue>, BuildHasherDefault<FxHasher>> =
735 IndexMap::default();
736
737 // 4. Let iteratorRecord be ? GetIterator(items, sync).
738 let mut iterator = items.get_iterator(IteratorHint::Sync, context)?;
739
740 // 5. Let k be 0.
741 let mut k = 0u64;
742
743 // 6. Repeat,
744 loop {
745 // a. If k ≥ 2^53 - 1, then
746 if k >= Number::MAX_SAFE_INTEGER as u64 {
747 // i. Let error be ThrowCompletion(a newly created TypeError object).
748 let error = JsNativeError::typ()
749 .with_message("exceeded maximum safe integer")
750 .into();
751
752 // ii. Return ? IteratorClose(iteratorRecord, error).
753 return iterator.close(Err(error), context);
754 }
755
756 // b. Let next be ? IteratorStepValue(iteratorRecord).
757 let Some(next) = iterator.step_value(context)? else {
758 // c. If next is false, then
759 // i. Return groups.
760 break;
761 };
762
763 // d. Let value be next.
764 let value = next;
765
766 // e. Let key be Completion(Call(callbackfn, undefined, « value, 𝔽(k) »)).
767 let key = callback.call(&JsValue::undefined(), &[value.clone(), k.into()], context);
768
769 // f. IfAbruptCloseIterator(key, iteratorRecord).
770 let key = if_abrupt_close_iterator!(key, iterator, context);
771
772 // h. Else,
773 // i. Assert: keyCoercion is collection.
774 // ii. Set key to CanonicalizeKeyedCollectionKey(key).
775 let key = canonicalize_keyed_collection_key(key);
776
777 // i. Perform AddValueToKeyedGroup(groups, key, value).
778 groups.entry(key).or_default().push(value);
779
780 // j. Set k to k + 1.
781 k += 1;
782 }
783
784 // 2. Let map be ! Construct(%Map%).
785 let mut map: OrderedMap<JsValue> = OrderedMap::new();
786
787 // 3. For each Record { [[Key]], [[Elements]] } g of groups, do
788 for (key, elements) in groups {
789 // a. Let elements be CreateArrayFromList(g.[[Elements]]).
790 let elements = Array::create_array_from_list(elements, context);
791
792 // b. Let entry be the Record { [[Key]]: g.[[Key]], [[Value]]: elements }.
793 // c. Append entry to map.[[MapData]].
794 map.insert(key, elements.into());
795 }
796
797 let proto = context.intrinsics().constructors().map().prototype();
798
799 // 4. Return map.
800 Ok(
801 JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), proto, map)
802 .into(),
803 )
804 }
805}
806
807/// `AddEntriesFromIterable`
808///
809/// Allows adding entries to a map from any object that has a `@@Iterator` field.
810///
811/// More information:
812/// - [ECMAScript reference][spec]
813///
814/// [spec]: https://tc39.es/ecma262/#sec-add-entries-from-iterable
815pub(crate) fn add_entries_from_iterable(
816 target: &JsObject,
817 iterable: &JsValue,
818 adder: &JsFunction,
819 context: &mut Context,
820) -> JsResult<JsValue> {
821 // 1. Let iteratorRecord be ? GetIterator(iterable, sync).
822 let mut iterator_record = iterable.get_iterator(IteratorHint::Sync, context)?;
823
824 // 2. Repeat,
825 // a. Let next be ? IteratorStepValue(iteratorRecord).
826 // b. If next is done, return target.
827 while let Some(next) = iterator_record.step_value(context)? {
828 let Some(next) = next.as_object() else {
829 // c. If next is not an Object, then
830 // i. Let error be ThrowCompletion(a newly created TypeError object).
831 // ii. Return ? IteratorClose(iteratorRecord, error).
832 let err = Err(JsNativeError::typ()
833 .with_message("cannot get key and value from primitive item of `iterable`")
834 .into());
835
836 // ii. Return ? IteratorClose(iteratorRecord, error).
837 return iterator_record.close(err, context);
838 };
839
840 // d. Let k be Completion(Get(next, "0")).
841 // e. IfAbruptCloseIterator(k, iteratorRecord).
842 let key = if_abrupt_close_iterator!(next.get(0, context), iterator_record, context);
843
844 // f. Let v be Completion(Get(next, "1")).
845 // g. IfAbruptCloseIterator(v, iteratorRecord).
846 let value = if_abrupt_close_iterator!(next.get(1, context), iterator_record, context);
847
848 // h. Let status be Completion(Call(adder, target, « k, v »)).
849 // i. IfAbruptCloseIterator(status, iteratorRecord).
850 let status = adder.call(&target.clone().into(), &[key, value], context);
851 if_abrupt_close_iterator!(status, iterator_record, context);
852 }
853
854 Ok(target.clone().into())
855}