Skip to main content

boa_engine/builtins/symbol/
mod.rs

1//! Boa's implementation of ECMAScript's global `Symbol` object.
2//!
3//! The data type symbol is a primitive data type.
4//! The `Symbol()` function returns a value of type symbol, has static properties that expose
5//! several members of built-in objects, has static methods that expose the global symbol registry,
6//! and resembles a built-in object class, but is incomplete as a constructor because it does not
7//! support the syntax "`new Symbol()`".
8//!
9//! Every symbol value returned from `Symbol()` is unique.
10//!
11//! More information:
12//! - [MDN documentation][mdn]
13//! - [ECMAScript reference][spec]
14//!
15//! [spec]: https://tc39.es/ecma262/#sec-symbol-value
16//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
17
18#[cfg(test)]
19mod tests;
20
21use std::{hash::BuildHasherDefault, sync::LazyLock};
22
23use crate::{
24    Context, JsArgs, JsResult, JsString,
25    builtins::BuiltInObject,
26    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
27    error::JsNativeError,
28    js_string,
29    object::JsObject,
30    property::Attribute,
31    realm::Realm,
32    string::StaticJsStrings,
33    symbol::JsSymbol,
34    value::JsValue,
35};
36use dashmap::DashMap;
37use rustc_hash::FxHasher;
38
39use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
40
41static GLOBAL_SYMBOL_REGISTRY: LazyLock<GlobalSymbolRegistry> =
42    LazyLock::new(GlobalSymbolRegistry::new);
43
44type FxDashMap<K, V> = DashMap<K, V, BuildHasherDefault<FxHasher>>;
45
46// We previously used `JsString` instead of `Box<[u16]>` for this, but since the glocal symbol
47// registry needed to be global, we had to either make `JsString` thread-safe or directly store
48// its info into the registry. `JsSymbol` is already a pretty niche feature of JS, and we expect only
49// advanced users to utilize it. On the other hand, almost every JS programmer uses `JsString`s, and
50// the first option would impact performance for all `JsString`s in general. For those reasons, we
51// opted for the second option, but we should try to optimize this in the future.
52struct GlobalSymbolRegistry {
53    keys: FxDashMap<Box<[u16]>, JsSymbol>,
54    symbols: FxDashMap<JsSymbol, Box<[u16]>>,
55}
56
57impl GlobalSymbolRegistry {
58    fn new() -> Self {
59        Self {
60            keys: FxDashMap::default(),
61            symbols: FxDashMap::default(),
62        }
63    }
64
65    fn get_or_create_symbol(&self, key: &JsString) -> JsResult<JsSymbol> {
66        let slice = key.iter().collect::<Vec<_>>();
67        if let Some(symbol) = self.keys.get(&slice[..]) {
68            return Ok(symbol.clone());
69        }
70
71        let symbol = JsSymbol::new(Some(key.clone())).ok_or_else(|| {
72            JsNativeError::range()
73                .with_message("reached the maximum number of symbols that can be created")
74        })?;
75        self.keys
76            .insert(slice.clone().into_boxed_slice(), symbol.clone());
77        self.symbols
78            .insert(symbol.clone(), slice.into_boxed_slice());
79        Ok(symbol)
80    }
81
82    fn get_key(&self, sym: &JsSymbol) -> Option<JsString> {
83        if let Some(key) = self.symbols.get(sym) {
84            return Some(js_string!(&**key));
85        }
86
87        None
88    }
89}
90
91/// The internal representation of a `Symbol` object.
92#[derive(Debug, Clone, Copy)]
93pub struct Symbol;
94
95impl IntrinsicObject for Symbol {
96    fn init(realm: &Realm) {
97        let symbol_async_iterator = JsSymbol::async_iterator();
98        let symbol_has_instance = JsSymbol::has_instance();
99        let symbol_is_concat_spreadable = JsSymbol::is_concat_spreadable();
100        let symbol_iterator = JsSymbol::iterator();
101        let symbol_match = JsSymbol::r#match();
102        let symbol_match_all = JsSymbol::match_all();
103        let symbol_replace = JsSymbol::replace();
104        let symbol_search = JsSymbol::search();
105        let symbol_species = JsSymbol::species();
106        let symbol_split = JsSymbol::split();
107        let symbol_to_primitive = JsSymbol::to_primitive();
108        let symbol_to_string_tag = JsSymbol::to_string_tag();
109        let symbol_unscopables = JsSymbol::unscopables();
110        let symbol_dispose = JsSymbol::dispose();
111        let symbol_async_dispose = JsSymbol::async_dispose();
112
113        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
114
115        let to_primitive = BuiltInBuilder::callable(realm, Self::to_primitive)
116            .name(js_string!("[Symbol.toPrimitive]"))
117            .length(1)
118            .build();
119
120        let get_description = BuiltInBuilder::callable(realm, Self::get_description)
121            .name(js_string!("get description"))
122            .build();
123
124        BuiltInBuilder::from_standard_constructor::<Self>(realm)
125            .static_method(Self::for_, js_string!("for"), 1)
126            .static_method(Self::key_for, js_string!("keyFor"), 1)
127            .static_property(
128                js_string!("asyncIterator"),
129                symbol_async_iterator,
130                attribute,
131            )
132            .static_property(js_string!("hasInstance"), symbol_has_instance, attribute)
133            .static_property(
134                js_string!("isConcatSpreadable"),
135                symbol_is_concat_spreadable,
136                attribute,
137            )
138            .static_property(js_string!("iterator"), symbol_iterator, attribute)
139            .static_property(js_string!("match"), symbol_match, attribute)
140            .static_property(js_string!("matchAll"), symbol_match_all, attribute)
141            .static_property(js_string!("replace"), symbol_replace, attribute)
142            .static_property(js_string!("search"), symbol_search, attribute)
143            .static_property(js_string!("species"), symbol_species, attribute)
144            .static_property(js_string!("split"), symbol_split, attribute)
145            .static_property(
146                js_string!("toPrimitive"),
147                symbol_to_primitive.clone(),
148                attribute,
149            )
150            .static_property(
151                js_string!("toStringTag"),
152                symbol_to_string_tag.clone(),
153                attribute,
154            )
155            .static_property(js_string!("unscopables"), symbol_unscopables, attribute)
156            .static_property(js_string!("dispose"), symbol_dispose, attribute)
157            .static_property(js_string!("asyncDispose"), symbol_async_dispose, attribute)
158            .method(Self::to_string, js_string!("toString"), 0)
159            .method(Self::value_of, js_string!("valueOf"), 0)
160            .accessor(
161                js_string!("description"),
162                Some(get_description),
163                None,
164                Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE,
165            )
166            .property(
167                symbol_to_string_tag,
168                Self::NAME,
169                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
170            )
171            .property(
172                symbol_to_primitive,
173                to_primitive,
174                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
175            )
176            .build();
177    }
178
179    fn get(intrinsics: &Intrinsics) -> JsObject {
180        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
181    }
182}
183
184impl BuiltInObject for Symbol {
185    const NAME: JsString = StaticJsStrings::SYMBOL;
186}
187
188impl BuiltInConstructor for Symbol {
189    const CONSTRUCTOR_ARGUMENTS: usize = 0;
190    const PROTOTYPE_STORAGE_SLOTS: usize = 6;
191    const CONSTRUCTOR_STORAGE_SLOTS: usize = 17;
192
193    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
194        StandardConstructors::symbol;
195
196    /// The `Symbol()` constructor returns a value of type symbol.
197    ///
198    /// It is incomplete as a constructor because it does not support
199    /// the syntax `new Symbol()` and it is not intended to be subclassed.
200    ///
201    /// More information:
202    /// - [ECMAScript reference][spec]
203    /// - [MDN documentation][mdn]
204    ///
205    /// [spec]: https://tc39.es/ecma262/#sec-symbol-description
206    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol
207    fn constructor(
208        new_target: &JsValue,
209        args: &[JsValue],
210        context: &mut Context,
211    ) -> JsResult<JsValue> {
212        // 1. If NewTarget is not undefined, throw a TypeError exception.
213        if !new_target.is_undefined() {
214            return Err(JsNativeError::typ()
215                .with_message("Symbol is not a constructor")
216                .into());
217        }
218
219        // 2. If description is undefined, let descString be undefined.
220        // 3. Else, let descString be ? ToString(description).
221        let description = match args.first() {
222            Some(value) if !value.is_undefined() => Some(value.to_string(context)?),
223            _ => None,
224        };
225
226        // 4. Return a new unique Symbol value whose [[Description]] value is descString.
227        Ok(JsSymbol::new(description)
228            .ok_or_else(|| {
229                JsNativeError::range()
230                    .with_message("reached the maximum number of symbols that can be created")
231            })?
232            .into())
233    }
234}
235
236impl Symbol {
237    fn this_symbol_value(value: &JsValue) -> JsResult<JsSymbol> {
238        value
239            .as_symbol()
240            .or_else(|| {
241                value
242                    .as_object()
243                    .and_then(|obj| obj.downcast_ref::<JsSymbol>().as_deref().cloned())
244            })
245            .ok_or_else(|| {
246                JsNativeError::typ()
247                    .with_message("'this' is not a Symbol")
248                    .into()
249            })
250    }
251
252    /// `Symbol.prototype.toString()`
253    ///
254    /// This method returns a string representing the specified `Symbol` object.
255    ///
256    /// More information:
257    /// - [MDN documentation][mdn]
258    /// - [ECMAScript reference][spec]
259    ///
260    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.tostring
261    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toString
262    #[allow(clippy::wrong_self_convention)]
263    pub(crate) fn to_string(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
264        // 1. Let sym be ? thisSymbolValue(this value).
265        let symbol = Self::this_symbol_value(this)?;
266
267        // 2. Return SymbolDescriptiveString(sym).
268        Ok(symbol.descriptive_string().into())
269    }
270
271    /// `Symbol.prototype.valueOf()`
272    ///
273    /// This method returns a `Symbol` that is the primitive value of the specified `Symbol` object.
274    ///
275    /// More information:
276    /// - [MDN documentation][mdn]
277    /// - [ECMAScript reference][spec]
278    ///
279    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/valueOf
280    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.valueof
281    pub(crate) fn value_of(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
282        // 1. Return ? thisSymbolValue(this value).
283        let symbol = Self::this_symbol_value(this)?;
284        Ok(symbol.into())
285    }
286
287    /// `get Symbol.prototype.description`
288    ///
289    /// This accessor returns the description of the `Symbol` object.
290    ///
291    /// More information:
292    /// - [MDN documentation][mdn]
293    /// - [ECMAScript reference][spec]
294    ///
295    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.description
296    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/description
297    pub(crate) fn get_description(
298        this: &JsValue,
299        _: &[JsValue],
300        _: &mut Context,
301    ) -> JsResult<JsValue> {
302        // 1. Let s be the this value.
303        // 2. Let sym be ? thisSymbolValue(s).
304        let sym = Self::this_symbol_value(this)?;
305
306        // 3. Return sym.[[Description]].
307        Ok(sym
308            .description()
309            .map_or(JsValue::undefined(), JsValue::from))
310    }
311
312    /// `Symbol.for( key )`
313    ///
314    /// More information:
315    /// - [MDN documentation][mdn]
316    /// - [ECMAScript reference][spec]
317    ///
318    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.for
319    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
320    pub(crate) fn for_(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
321        // 1. Let stringKey be ? ToString(key).
322        let string_key = args
323            .first()
324            .cloned()
325            .unwrap_or_default()
326            .to_string(context)?;
327        // 2. For each element e of the GlobalSymbolRegistry List, do
328        //     a. If SameValue(e.[[Key]], stringKey) is true, return e.[[Symbol]].
329        // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for stringKey.
330        // 4. Let newSymbol be a new unique Symbol value whose [[Description]] value is stringKey.
331        // 5. Append the Record { [[Key]]: stringKey, [[Symbol]]: newSymbol } to the GlobalSymbolRegistry List.
332        // 6. Return newSymbol.
333        GLOBAL_SYMBOL_REGISTRY
334            .get_or_create_symbol(&string_key)
335            .map(JsValue::from)
336    }
337
338    /// `Symbol.keyFor( sym )`
339    ///
340    ///
341    /// More information:
342    /// - [MDN documentation][mdn]
343    /// - [ECMAScript reference][spec]
344    ///
345    /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.keyfor
346    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/keyFor
347    pub(crate) fn key_for(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
348        // 1. If Type(sym) is not Symbol, throw a TypeError exception.
349        let sym = args.get_or_undefined(0).as_symbol().ok_or_else(|| {
350            JsNativeError::typ().with_message("Symbol.keyFor: sym is not a symbol")
351        })?;
352
353        // 2. For each element e of the GlobalSymbolRegistry List (see 20.4.2.2), do
354        //     a. If SameValue(e.[[Symbol]], sym) is true, return e.[[Key]].
355        // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for sym.
356        // 4. Return undefined.
357
358        Ok(GLOBAL_SYMBOL_REGISTRY
359            .get_key(&sym)
360            .map(JsValue::from)
361            .unwrap_or_default())
362    }
363
364    /// `Symbol.prototype [ @@toPrimitive ]`
365    ///
366    /// This function is called by ECMAScript language operators to convert a Symbol object to a primitive value.
367    /// NOTE: The argument is ignored
368    ///
369    /// More information:
370    /// - [MDN documentation][mdn]
371    /// - [ECMAScript reference][spec]
372    ///
373    /// [spec]: https://tc39.es/ecma262/multipage/#sec-symbol.prototype-@@toprimitive
374    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/@@toPrimitive
375    pub(crate) fn to_primitive(
376        this: &JsValue,
377        _: &[JsValue],
378        _: &mut Context,
379    ) -> JsResult<JsValue> {
380        let sym = Self::this_symbol_value(this)?;
381        // 1. Return ? thisSymbolValue(this value).
382        Ok(sym.into())
383    }
384}