boa/builtins/map/
mod.rs

1//! This module implements the global `Map` object.
2//!
3//! The JavaScript `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
13#![allow(clippy::mutable_key_type)]
14
15use crate::{
16    builtins::{
17        iterable::{get_iterator, IteratorResult},
18        BuiltIn,
19    },
20    context::StandardObjects,
21    object::{
22        internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder,
23        JsObject, ObjectData,
24    },
25    property::{Attribute, PropertyNameKind},
26    symbol::WellKnownSymbols,
27    BoaProfiler, Context, JsResult, JsValue,
28};
29use ordered_map::OrderedMap;
30
31pub mod map_iterator;
32use map_iterator::MapIterator;
33
34use super::JsArgs;
35use num_traits::Zero;
36
37pub mod ordered_map;
38#[cfg(test)]
39mod tests;
40
41#[derive(Debug, Clone)]
42pub(crate) struct Map(OrderedMap<JsValue>);
43
44impl BuiltIn for Map {
45    const NAME: &'static str = "Map";
46
47    fn attribute() -> Attribute {
48        Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
49    }
50
51    fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
52        let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
53
54        let get_species = FunctionBuilder::native(context, Self::get_species)
55            .name("get [Symbol.species]")
56            .constructable(false)
57            .build();
58
59        let get_size = FunctionBuilder::native(context, Self::get_size)
60            .name("get size")
61            .length(0)
62            .constructable(false)
63            .build();
64
65        let entries_function = FunctionBuilder::native(context, Self::entries)
66            .name("entries")
67            .length(0)
68            .constructable(false)
69            .build();
70
71        let map_object = ConstructorBuilder::with_standard_object(
72            context,
73            Self::constructor,
74            context.standard_objects().map_object().clone(),
75        )
76        .name(Self::NAME)
77        .length(Self::LENGTH)
78        .static_accessor(
79            WellKnownSymbols::species(),
80            Some(get_species),
81            None,
82            Attribute::CONFIGURABLE,
83        )
84        .property(
85            "entries",
86            entries_function.clone(),
87            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
88        )
89        .property(
90            WellKnownSymbols::iterator(),
91            entries_function,
92            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
93        )
94        .property(
95            WellKnownSymbols::to_string_tag(),
96            Self::NAME,
97            Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
98        )
99        .method(Self::clear, "clear", 0)
100        .method(Self::delete, "delete", 1)
101        .method(Self::for_each, "forEach", 1)
102        .method(Self::get, "get", 1)
103        .method(Self::has, "has", 1)
104        .method(Self::keys, "keys", 0)
105        .method(Self::set, "set", 2)
106        .method(Self::values, "values", 0)
107        .accessor("size", Some(get_size), None, Attribute::CONFIGURABLE)
108        .build();
109
110        (Self::NAME, map_object.into(), Self::attribute())
111    }
112}
113
114impl Map {
115    pub(crate) const LENGTH: usize = 0;
116
117    /// `Map ( [ iterable ] )`
118    ///
119    /// Constructor for `Map` objects.
120    ///
121    /// More information:
122    ///  - [ECMAScript reference][spec]
123    ///  - [MDN documentation][mdn]
124    ///
125    /// [spec]: https://tc39.es/ecma262/#sec-map-iterable
126    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map
127    pub(crate) fn constructor(
128        new_target: &JsValue,
129        args: &[JsValue],
130        context: &mut Context,
131    ) -> JsResult<JsValue> {
132        // 1. If NewTarget is undefined, throw a TypeError exception.
133        if new_target.is_undefined() {
134            return context
135                .throw_type_error("calling a builtin Map constructor without new is forbidden");
136        }
137
138        // 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%Map.prototype%", « [[MapData]] »).
139        let prototype =
140            get_prototype_from_constructor(new_target, StandardObjects::map_object, context)?;
141        let map = context.construct_object();
142        map.set_prototype_instance(prototype.into());
143
144        // 3. Set map.[[MapData]] to a new empty List.
145        map.borrow_mut().data = ObjectData::map(OrderedMap::new());
146
147        // 4. If iterable is either undefined or null, return map.
148        let iterable = match args.get_or_undefined(0) {
149            val if !val.is_null_or_undefined() => val,
150            _ => return Ok(map.into()),
151        };
152
153        // 5. Let adder be ? Get(map, "set").
154        let adder = map.get("set", context)?;
155
156        // 6. Return ? AddEntriesFromIterable(map, iterable, adder).
157        add_entries_from_iterable(&map, iterable, &adder, context)
158    }
159
160    /// `get Map [ @@species ]`
161    ///
162    /// The `Map [ @@species ]` accessor property returns the Map constructor.
163    ///
164    /// More information:
165    ///  - [ECMAScript reference][spec]
166    ///  - [MDN documentation][mdn]
167    ///
168    /// [spec]: https://tc39.es/ecma262/#sec-get-map-@@species
169    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species
170    fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
171        // 1. Return the this value.
172        Ok(this.clone())
173    }
174
175    /// `Map.prototype.entries()`
176    ///
177    /// Returns a new Iterator object that contains the [key, value] pairs for each element in the Map object in insertion order.
178    ///
179    /// More information:
180    ///  - [ECMAScript reference][spec]
181    ///  - [MDN documentation][mdn]
182    ///
183    /// [spec]: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-map.prototype.entries
184    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
185    pub(crate) fn entries(
186        this: &JsValue,
187        _: &[JsValue],
188        context: &mut Context,
189    ) -> JsResult<JsValue> {
190        // 1. Let M be the this value.
191        // 2. Return ? CreateMapIterator(M, key+value).
192        MapIterator::create_map_iterator(this, PropertyNameKind::KeyAndValue, context)
193    }
194
195    /// `Map.prototype.keys()`
196    ///
197    /// Returns a new Iterator object that contains the keys for each element in the Map object in insertion order.
198    ///
199    /// More information:
200    ///  - [ECMAScript reference][spec]
201    ///  - [MDN documentation][mdn]
202    ///
203    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.keys
204    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
205    pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
206        // 1. Let M be the this value.
207        // 2. Return ? CreateMapIterator(M, key).
208        MapIterator::create_map_iterator(this, PropertyNameKind::Key, context)
209    }
210
211    /// `Map.prototype.set( key, value )`
212    ///
213    /// Inserts a new entry in the Map object.
214    ///
215    /// More information:
216    ///  - [ECMAScript reference][spec]
217    ///  - [MDN documentation][mdn]
218    ///
219    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.set
220    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set
221    pub(crate) fn set(
222        this: &JsValue,
223        args: &[JsValue],
224        context: &mut Context,
225    ) -> JsResult<JsValue> {
226        let key = args.get_or_undefined(0);
227        let value = args.get_or_undefined(1);
228
229        // 1. Let M be the this value.
230        if let Some(object) = this.as_object() {
231            // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
232            // 3. Let entries be the List that is M.[[MapData]].
233            if let Some(map) = object.borrow_mut().as_map_mut() {
234                let key = match key {
235                    JsValue::Rational(r) => {
236                        // 5. If key is -0𝔽, set key to +0𝔽.
237                        if r.is_zero() {
238                            JsValue::Rational(0f64)
239                        } else {
240                            key.clone()
241                        }
242                    }
243                    _ => key.clone(),
244                };
245                // 4. For each Record { [[Key]], [[Value]] } p of entries, do
246                // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then
247                // i. Set p.[[Value]] to value.
248                // 6. Let p be the Record { [[Key]]: key, [[Value]]: value }.
249                // 7. Append p as the last element of entries.
250                map.insert(key, value.clone());
251                // ii. Return M.
252                // 8. Return M.
253                return Ok(this.clone());
254            }
255        }
256        context.throw_type_error("'this' is not a Map")
257    }
258
259    /// `get Map.prototype.size`
260    ///
261    /// Obtains the size of the map, filtering empty keys to ensure it updates
262    /// while iterating.
263    ///
264    /// More information:
265    ///  - [ECMAScript reference][spec]
266    ///  - [MDN documentation][mdn]
267    ///
268    /// [spec]: https://tc39.es/ecma262/#sec-get-map.prototype.size
269    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size
270    pub(crate) fn get_size(
271        this: &JsValue,
272        _: &[JsValue],
273        context: &mut Context,
274    ) -> JsResult<JsValue> {
275        // 1. Let M be the this value.
276        if let Some(object) = this.as_object() {
277            // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
278            // 3. Let entries be the List that is M.[[MapData]].
279            if let Some(map) = object.borrow_mut().as_map_mut() {
280                // 4. Let count be 0.
281                // 5. For each Record { [[Key]], [[Value]] } p of entries, do
282                // a. If p.[[Key]] is not empty, set count to count + 1.
283                // 6. Return 𝔽(count).
284                return Ok(map.len().into());
285            }
286        }
287        context.throw_type_error("'this' is not a Map")
288    }
289
290    /// `Map.prototype.delete( key )`
291    ///
292    /// Removes the element associated with the key, if it exists.
293    /// Returns true if there was an element, and false otherwise.
294    ///
295    /// More information:
296    ///  - [ECMAScript reference][spec]
297    ///  - [MDN documentation][mdn]
298    ///
299    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.delete
300    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete
301    pub(crate) fn delete(
302        this: &JsValue,
303        args: &[JsValue],
304        context: &mut Context,
305    ) -> JsResult<JsValue> {
306        let key = args.get_or_undefined(0);
307
308        // 1. Let M be the this value.
309        if let Some(object) = this.as_object() {
310            // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
311            // 3. Let entries be the List that is M.[[MapData]].
312            if let Some(map) = object.borrow_mut().as_map_mut() {
313                // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then
314                // i. Set p.[[Key]] to empty.
315                // ii. Set p.[[Value]] to empty.
316                // iii. Return true.
317                // 5. Return false.
318                return Ok(map.remove(key).is_some().into());
319            }
320        }
321        context.throw_type_error("'this' is not a Map")
322    }
323
324    /// `Map.prototype.get( key )`
325    ///
326    /// Returns the value associated with the key, or undefined if there is none.
327    ///
328    /// More information:
329    ///  - [ECMAScript reference][spec]
330    ///  - [MDN documentation][mdn]
331    ///
332    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.get
333    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
334    pub(crate) fn get(
335        this: &JsValue,
336        args: &[JsValue],
337        context: &mut Context,
338    ) -> JsResult<JsValue> {
339        let key = args.get_or_undefined(0);
340
341        const JS_ZERO: &JsValue = &JsValue::Rational(0f64);
342
343        let key = match key {
344            JsValue::Rational(r) => {
345                if r.is_zero() {
346                    JS_ZERO
347                } else {
348                    key
349                }
350            }
351            _ => key,
352        };
353
354        // 1. Let M be the this value.
355        if let JsValue::Object(ref object) = this {
356            // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
357            // 3. Let entries be the List that is M.[[MapData]].
358            if let Some(map) = object.borrow().as_map_ref() {
359                // 4. For each Record { [[Key]], [[Value]] } p of entries, do
360                // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return p.[[Value]].
361                // 5. Return undefined.
362                return Ok(map.get(key).cloned().unwrap_or_default());
363            }
364        }
365
366        context.throw_type_error("'this' is not a Map")
367    }
368
369    /// `Map.prototype.clear( )`
370    ///
371    /// Removes all entries from the map.
372    ///
373    /// More information:
374    ///  - [ECMAScript reference][spec]
375    ///  - [MDN documentation][mdn]
376    ///
377    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.clear
378    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear
379    pub(crate) fn clear(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
380        // 1. Let M be the this value.
381        // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
382        if let Some(object) = this.as_object() {
383            // 3. Let entries be the List that is M.[[MapData]].
384            if let Some(map) = object.borrow_mut().as_map_mut() {
385                // 4. For each Record { [[Key]], [[Value]] } p of entries, do
386                // a. Set p.[[Key]] to empty.
387                // b. Set p.[[Value]] to empty.
388                map.clear();
389
390                // 5. Return undefined.
391                return Ok(JsValue::undefined());
392            }
393        }
394        context.throw_type_error("'this' is not a Map")
395    }
396
397    /// `Map.prototype.has( key )`
398    ///
399    /// Checks if the map contains an entry with the given key.
400    ///
401    /// More information:
402    ///  - [ECMAScript reference][spec]
403    ///  - [MDN documentation][mdn]
404    ///
405    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has
406    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
407    pub(crate) fn has(
408        this: &JsValue,
409        args: &[JsValue],
410        context: &mut Context,
411    ) -> JsResult<JsValue> {
412        let key = args.get_or_undefined(0);
413
414        const JS_ZERO: &JsValue = &JsValue::Rational(0f64);
415
416        let key = match key {
417            JsValue::Rational(r) => {
418                if r.is_zero() {
419                    JS_ZERO
420                } else {
421                    key
422                }
423            }
424            _ => key,
425        };
426
427        // 1. Let M be the this value.
428        if let JsValue::Object(ref object) = this {
429            // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
430            // 3. Let entries be the List that is M.[[MapData]].
431            if let Some(map) = object.borrow().as_map_ref() {
432                // 4. For each Record { [[Key]], [[Value]] } p of entries, do
433                // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return true.
434                // 5. Return false.
435                return Ok(map.contains_key(key).into());
436            }
437        }
438
439        context.throw_type_error("'this' is not a Map")
440    }
441
442    /// `Map.prototype.forEach( callbackFn [ , thisArg ] )`
443    ///
444    /// Executes the provided callback function for each key-value pair in the map.
445    ///
446    /// More information:
447    ///  - [ECMAScript reference][spec]
448    ///  - [MDN documentation][mdn]
449    ///
450    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.foreach
451    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
452    pub(crate) fn for_each(
453        this: &JsValue,
454        args: &[JsValue],
455        context: &mut Context,
456    ) -> JsResult<JsValue> {
457        // 1. Let M be the this value.
458        // 2. Perform ? RequireInternalSlot(M, [[MapData]]).
459        let map = match this {
460            JsValue::Object(obj) if obj.is_map() => obj,
461            _ => return context.throw_type_error("`this` is not a Map"),
462        };
463
464        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
465        let callback = match args.get_or_undefined(0) {
466            JsValue::Object(obj) if obj.is_callable() => obj,
467            val => {
468                let name = val.to_string(context)?;
469                return context.throw_type_error(format!("{} is not a function", name));
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 = map
485            .borrow_mut()
486            .as_map_mut()
487            .expect("checked that `this` was a map")
488            .lock(map.clone());
489
490        // 4. Let entries be the List that is M.[[MapData]].
491        // 5. For each Record { [[Key]], [[Value]] } e of entries, do
492        let mut index = 0;
493        loop {
494            let arguments = {
495                let map = map.borrow();
496                let map = map.as_map_ref().expect("checked that `this` was a map");
497                if index < map.full_len() {
498                    map.get_index(index)
499                        .map(|(k, v)| [v.clone(), k.clone(), this.clone()])
500                } else {
501                    // 6. Return undefined.
502                    return Ok(JsValue::undefined());
503                }
504            };
505
506            // a. If e.[[Key]] is not empty, then
507            if let Some(arguments) = arguments {
508                // i. Perform ? Call(callbackfn, thisArg, « e.[[Value]], e.[[Key]], M »).
509                callback.call(this_arg, &arguments, context)?;
510            }
511
512            index += 1;
513        }
514    }
515
516    /// `Map.prototype.values()`
517    ///
518    /// Returns a new Iterator object that contains the values for each element in the Map object in insertion order.
519    ///
520    /// More information:
521    ///  - [ECMAScript reference][spec]
522    ///  - [MDN documentation][mdn]
523    ///
524    /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.values
525    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
526    pub(crate) fn values(
527        this: &JsValue,
528        _: &[JsValue],
529        context: &mut Context,
530    ) -> JsResult<JsValue> {
531        // 1. Let M be the this value.
532        // 2. Return ? CreateMapIterator(M, value).
533        MapIterator::create_map_iterator(this, PropertyNameKind::Value, context)
534    }
535}
536
537/// `AddEntriesFromIterable`
538///
539/// Allows adding entries to a map from any object that has a `@@Iterator` field.
540///
541/// More information:
542///  - [ECMAScript reference][spec]
543///
544/// [spec]: https://tc39.es/ecma262/#sec-add-entries-from-iterable
545pub(crate) fn add_entries_from_iterable(
546    target: &JsObject,
547    iterable: &JsValue,
548    adder: &JsValue,
549    context: &mut Context,
550) -> JsResult<JsValue> {
551    // 1. If IsCallable(adder) is false, throw a TypeError exception.
552    let adder = match adder {
553        JsValue::Object(obj) if obj.is_callable() => obj,
554        _ => return context.throw_type_error("property `set` of `NewTarget` is not callable"),
555    };
556
557    // 2. Let iteratorRecord be ? GetIterator(iterable).
558    let iterator_record = get_iterator(iterable, context)?;
559
560    // 3. Repeat,
561    loop {
562        // a. Let next be ? IteratorStep(iteratorRecord).
563        // c. Let nextItem be ? IteratorValue(next).
564        let IteratorResult { value, done } = iterator_record.next(context)?;
565
566        // b. If next is false, return target.
567        if done {
568            return Ok(target.clone().into());
569        }
570
571        let next_item = if let Some(obj) = value.as_object() {
572            obj
573        }
574        // d. If Type(nextItem) is not Object, then
575        else {
576            // i. Let error be ThrowCompletion(a newly created TypeError object).
577            let err = context
578                .throw_type_error("cannot get key and value from primitive item of `iterable`");
579
580            // ii. Return ? IteratorClose(iteratorRecord, error).
581            return iterator_record.close(err, context);
582        };
583
584        // e. Let k be Get(nextItem, "0").
585        // f. IfAbruptCloseIterator(k, iteratorRecord).
586        let key = match next_item.get(0, context) {
587            Ok(val) => val,
588            err => return iterator_record.close(err, context),
589        };
590
591        // g. Let v be Get(nextItem, "1").
592        // h. IfAbruptCloseIterator(v, iteratorRecord).
593        let value = match next_item.get(1, context) {
594            Ok(val) => val,
595            err => return iterator_record.close(err, context),
596        };
597
598        // i. Let status be Call(adder, target, « k, v »).
599        let status = adder.call(&target.clone().into(), &[key, value], context);
600
601        // j. IfAbruptCloseIterator(status, iteratorRecord).
602        if status.is_err() {
603            return iterator_record.close(status, context);
604        }
605    }
606}