Skip to main content

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}