Skip to main content

boa_engine/
symbol.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#![deny(
19    unsafe_op_in_unsafe_fn,
20    clippy::undocumented_unsafe_blocks,
21    clippy::missing_safety_doc
22)]
23
24use crate::{
25    js_string,
26    string::{JsString, StaticJsStrings},
27};
28use boa_gc::{Finalize, Trace};
29use tag_ptr::{Tagged, UnwrappedTagged};
30
31use boa_macros::{JsData, js_str};
32use num_enum::{IntoPrimitive, TryFromPrimitive};
33
34use std::{
35    hash::{Hash, Hasher},
36    mem::ManuallyDrop,
37    ptr::NonNull,
38    sync::{Arc, atomic::Ordering},
39};
40
41use portable_atomic::AtomicU64;
42
43/// Reserved number of symbols.
44///
45/// This is the maximum number of well known and internal engine symbols
46/// that can be defined.
47const RESERVED_SYMBOL_HASHES: u64 = 127;
48
49fn get_id() -> Option<u64> {
50    // Symbol hash.
51    //
52    // For now this is an incremented u64 number.
53    static SYMBOL_HASH_COUNT: AtomicU64 = AtomicU64::new(RESERVED_SYMBOL_HASHES + 1);
54
55    SYMBOL_HASH_COUNT
56        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
57            value.checked_add(1)
58        })
59        .ok()
60}
61
62/// List of well known symbols.
63#[derive(Debug, Clone, Copy, TryFromPrimitive, IntoPrimitive)]
64#[repr(u8)]
65enum WellKnown {
66    AsyncIterator,
67    HasInstance,
68    IsConcatSpreadable,
69    Iterator,
70    Match,
71    MatchAll,
72    Replace,
73    Search,
74    Species,
75    Split,
76    ToPrimitive,
77    ToStringTag,
78    Unscopables,
79    Dispose,
80    AsyncDispose,
81}
82
83impl WellKnown {
84    const fn description(self) -> JsString {
85        match self {
86            Self::AsyncIterator => StaticJsStrings::SYMBOL_ASYNC_ITERATOR,
87            Self::HasInstance => StaticJsStrings::SYMBOL_HAS_INSTANCE,
88            Self::IsConcatSpreadable => StaticJsStrings::SYMBOL_IS_CONCAT_SPREADABLE,
89            Self::Iterator => StaticJsStrings::SYMBOL_ITERATOR,
90            Self::Match => StaticJsStrings::SYMBOL_MATCH,
91            Self::MatchAll => StaticJsStrings::SYMBOL_MATCH_ALL,
92            Self::Replace => StaticJsStrings::SYMBOL_REPLACE,
93            Self::Search => StaticJsStrings::SYMBOL_SEARCH,
94            Self::Species => StaticJsStrings::SYMBOL_SPECIES,
95            Self::Split => StaticJsStrings::SYMBOL_SPLIT,
96            Self::ToPrimitive => StaticJsStrings::SYMBOL_TO_PRIMITIVE,
97            Self::ToStringTag => StaticJsStrings::SYMBOL_TO_STRING_TAG,
98            Self::Unscopables => StaticJsStrings::SYMBOL_UNSCOPABLES,
99            Self::Dispose => StaticJsStrings::SYMBOL_DISPOSE,
100            Self::AsyncDispose => StaticJsStrings::SYMBOL_ASYNC_DISPOSE,
101        }
102    }
103
104    const fn fn_name(self) -> JsString {
105        match self {
106            Self::AsyncIterator => StaticJsStrings::FN_SYMBOL_ASYNC_ITERATOR,
107            Self::HasInstance => StaticJsStrings::FN_SYMBOL_HAS_INSTANCE,
108            Self::IsConcatSpreadable => StaticJsStrings::FN_SYMBOL_IS_CONCAT_SPREADABLE,
109            Self::Iterator => StaticJsStrings::FN_SYMBOL_ITERATOR,
110            Self::Match => StaticJsStrings::FN_SYMBOL_MATCH,
111            Self::MatchAll => StaticJsStrings::FN_SYMBOL_MATCH_ALL,
112            Self::Replace => StaticJsStrings::FN_SYMBOL_REPLACE,
113            Self::Search => StaticJsStrings::FN_SYMBOL_SEARCH,
114            Self::Species => StaticJsStrings::FN_SYMBOL_SPECIES,
115            Self::Split => StaticJsStrings::FN_SYMBOL_SPLIT,
116            Self::ToPrimitive => StaticJsStrings::FN_SYMBOL_TO_PRIMITIVE,
117            Self::ToStringTag => StaticJsStrings::FN_SYMBOL_TO_STRING_TAG,
118            Self::Unscopables => StaticJsStrings::FN_SYMBOL_UNSCOPABLES,
119            Self::Dispose => StaticJsStrings::FN_SYMBOL_DISPOSE,
120            Self::AsyncDispose => StaticJsStrings::FN_SYMBOL_ASYNC_DISPOSE,
121        }
122    }
123
124    const fn hash(self) -> u64 {
125        self as u64
126    }
127
128    fn from_tag(tag: usize) -> Option<Self> {
129        Self::try_from_primitive(u8::try_from(tag).ok()?).ok()
130    }
131}
132
133/// The inner representation of a JavaScript symbol.
134#[derive(Debug, Clone)]
135pub(crate) struct RawJsSymbol {
136    hash: u64,
137    // must be a `Box`, since this needs to be shareable between many threads.
138    description: Option<Box<[u16]>>,
139}
140
141/// This represents a JavaScript symbol primitive.
142#[derive(Trace, Finalize, JsData)]
143// Safety: JsSymbol does not contain any objects which needs to be traced,
144// so this is safe.
145#[boa_gc(unsafe_empty_trace)]
146#[allow(clippy::module_name_repetitions)]
147pub struct JsSymbol {
148    repr: Tagged<RawJsSymbol>,
149}
150
151// SAFETY: `JsSymbol` uses `Arc` to do the reference counting, making this type thread-safe.
152unsafe impl Send for JsSymbol {}
153// SAFETY: `JsSymbol` uses `Arc` to do the reference counting, making this type thread-safe.
154unsafe impl Sync for JsSymbol {}
155
156macro_rules! well_known_symbols {
157    ( $( $(#[$attr:meta])* ($name:ident, $variant:path) ),+$(,)? ) => {
158        $(
159            $(#[$attr])* #[must_use] pub const fn $name() -> JsSymbol {
160                JsSymbol {
161                    // the cast shouldn't matter since we only have 127 const symbols
162                    repr: Tagged::from_tag($variant.hash() as usize),
163                }
164            }
165        )+
166    };
167}
168
169impl JsSymbol {
170    /// Creates a new symbol.
171    ///
172    /// Returns `None` if the maximum number of possible symbols has been reached (`u64::MAX`).
173    #[inline]
174    #[must_use]
175    pub fn new(description: Option<JsString>) -> Option<Self> {
176        let hash = get_id()?;
177        let arc = Arc::new(RawJsSymbol {
178            hash,
179            description: description.map(|s| s.iter().collect::<Vec<_>>().into_boxed_slice()),
180        });
181
182        Some(Self {
183            // SAFETY: Pointers returned by `Arc::into_raw` must be non-null.
184            repr: unsafe { Tagged::from_ptr(Arc::into_raw(arc).cast_mut()) },
185        })
186    }
187
188    /// Returns the `Symbol` description.
189    #[inline]
190    #[must_use]
191    pub fn description(&self) -> Option<JsString> {
192        match self.repr.unwrap() {
193            UnwrappedTagged::Ptr(ptr) => {
194                // SAFETY: `ptr` comes from `Arc`, which ensures the validity of the pointer
195                // as long as we correctly call `Arc::from_raw` on `Drop`.
196                unsafe { ptr.as_ref().description.as_ref().map(|v| js_string!(&**v)) }
197            }
198            UnwrappedTagged::Tag(tag) => {
199                // SAFETY: All tagged reprs always come from `WellKnown` itself, making
200                // this operation always safe.
201                let wk = unsafe { WellKnown::from_tag(tag).unwrap_unchecked() };
202                Some(wk.description())
203            }
204        }
205    }
206
207    /// Returns the `Symbol` as a function name.
208    ///
209    /// Equivalent to `[description]`, but returns the empty string if the symbol doesn't have a
210    /// description.
211    #[inline]
212    #[must_use]
213    pub fn fn_name(&self) -> JsString {
214        if let UnwrappedTagged::Tag(tag) = self.repr.unwrap() {
215            // SAFETY: All tagged reprs always come from `WellKnown` itself, making
216            // this operation always safe.
217            let wk = unsafe { WellKnown::from_tag(tag).unwrap_unchecked() };
218            return wk.fn_name();
219        }
220        self.description()
221            .map(|s| js_string!(js_str!("["), &s, js_str!("]")))
222            .unwrap_or_default()
223    }
224
225    /// Returns the `Symbol`s hash.
226    ///
227    /// The hash is guaranteed to be unique.
228    #[inline]
229    #[must_use]
230    pub fn hash(&self) -> u64 {
231        match self.repr.unwrap() {
232            UnwrappedTagged::Ptr(ptr) => {
233                // SAFETY: `ptr` comes from `Arc`, which ensures the validity of the pointer
234                // as long as we correctly call `Arc::from_raw` on `Drop`.
235                unsafe { ptr.as_ref().hash }
236            }
237            UnwrappedTagged::Tag(tag) => {
238                // SAFETY: All tagged reprs always come from `WellKnown` itself, making
239                // this operation always safe.
240                unsafe { WellKnown::from_tag(tag).unwrap_unchecked().hash() }
241            }
242        }
243    }
244
245    /// Abstract operation `SymbolDescriptiveString ( sym )`
246    ///
247    /// More info:
248    /// - [ECMAScript reference][spec]
249    ///
250    /// [spec]: https://tc39.es/ecma262/#sec-symboldescriptivestring
251    #[must_use]
252    pub fn descriptive_string(&self) -> JsString {
253        self.description().as_ref().map_or_else(
254            || js_string!("Symbol()"),
255            |desc| js_string!(js_str!("Symbol("), desc, js_str!(")")),
256        )
257    }
258
259    /// Consumes the [`JsSymbol`], returning a pointer to `RawJsSymbol`.
260    ///
261    /// To avoid a memory leak the pointer must be converted back to a `JsSymbol` using
262    /// [`JsSymbol::from_raw`].
263    #[inline]
264    #[must_use]
265    #[allow(unused, reason = "only used in nan-boxed implementation of JsValue")]
266    pub(crate) fn into_raw(self) -> NonNull<RawJsSymbol> {
267        ManuallyDrop::new(self).repr.as_inner_ptr()
268    }
269
270    /// Constructs a `JsSymbol` from a pointer to `RawJsSymbol`.
271    ///
272    /// The raw pointer must have been previously returned by a call to
273    /// [`JsSymbol::into_raw`].
274    ///
275    /// # Safety
276    ///
277    /// This function is unsafe because improper use may lead to memory unsafety,
278    /// even if the returned `JsSymbol` is never accessed.
279    #[inline]
280    #[must_use]
281    #[allow(unused, reason = "only used in nan-boxed implementation of JsValue")]
282    pub(crate) unsafe fn from_raw(ptr: NonNull<RawJsSymbol>) -> Self {
283        Self {
284            repr: Tagged::from_non_null(ptr),
285        }
286    }
287
288    well_known_symbols! {
289        /// Gets the static `JsSymbol` for `"Symbol.asyncIterator"`.
290        (async_iterator, WellKnown::AsyncIterator),
291        /// Gets the static `JsSymbol` for `"Symbol.hasInstance"`.
292        (has_instance, WellKnown::HasInstance),
293        /// Gets the static `JsSymbol` for `"Symbol.isConcatSpreadable"`.
294        (is_concat_spreadable, WellKnown::IsConcatSpreadable),
295        /// Gets the static `JsSymbol` for `"Symbol.iterator"`.
296        (iterator, WellKnown::Iterator),
297        /// Gets the static `JsSymbol` for `"Symbol.match"`.
298        (r#match, WellKnown::Match),
299        /// Gets the static `JsSymbol` for `"Symbol.matchAll"`.
300        (match_all, WellKnown::MatchAll),
301        /// Gets the static `JsSymbol` for `"Symbol.replace"`.
302        (replace, WellKnown::Replace),
303        /// Gets the static `JsSymbol` for `"Symbol.search"`.
304        (search, WellKnown::Search),
305        /// Gets the static `JsSymbol` for `"Symbol.species"`.
306        (species, WellKnown::Species),
307        /// Gets the static `JsSymbol` for `"Symbol.split"`.
308        (split, WellKnown::Split),
309        /// Gets the static `JsSymbol` for `"Symbol.toPrimitive"`.
310        (to_primitive, WellKnown::ToPrimitive),
311        /// Gets the static `JsSymbol` for `"Symbol.toStringTag"`.
312        (to_string_tag, WellKnown::ToStringTag),
313        /// Gets the static `JsSymbol` for `"Symbol.unscopables"`.
314        (unscopables, WellKnown::Unscopables),
315        /// Gets the static `JsSymbol` for `"Symbol.dispose"`.
316        (dispose, WellKnown::Dispose),
317        /// Gets the static `JsSymbol` for `"Symbol.asyncDispose"`.
318        (async_dispose, WellKnown::AsyncDispose),
319    }
320}
321
322impl Clone for JsSymbol {
323    fn clone(&self) -> Self {
324        if let UnwrappedTagged::Ptr(ptr) = self.repr.unwrap() {
325            // SAFETY: the pointer returned by `self.repr` must be a valid pointer
326            // that came from an `Arc::into_raw` call.
327            unsafe {
328                let arc = Arc::from_raw(ptr.as_ptr().cast_const());
329                // Don't need the Arc since `self` is already a copyable pointer, just need to
330                // trigger the `clone` impl.
331                std::mem::forget(arc.clone());
332                std::mem::forget(arc);
333            }
334        }
335        Self { repr: self.repr }
336    }
337}
338
339impl Drop for JsSymbol {
340    fn drop(&mut self) {
341        if let UnwrappedTagged::Ptr(ptr) = self.repr.unwrap() {
342            // SAFETY: the pointer returned by `self.repr` must be a valid pointer
343            // that came from an `Arc::into_raw` call.
344            unsafe { drop(Arc::from_raw(ptr.as_ptr().cast_const())) }
345        }
346    }
347}
348
349impl std::fmt::Debug for JsSymbol {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        f.debug_struct("JsSymbol")
352            .field("hash", &self.hash())
353            .field("description", &self.description())
354            .finish()
355    }
356}
357
358impl std::fmt::Display for JsSymbol {
359    #[inline]
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        match &self.description() {
362            Some(desc) => write!(f, "Symbol({})", desc.to_std_string_escaped()),
363            None => write!(f, "Symbol()"),
364        }
365    }
366}
367
368impl Eq for JsSymbol {}
369
370impl PartialEq for JsSymbol {
371    #[inline]
372    fn eq(&self, other: &Self) -> bool {
373        self.hash() == other.hash()
374    }
375}
376
377impl PartialOrd for JsSymbol {
378    #[inline]
379    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
380        Some(self.cmp(other))
381    }
382}
383
384impl Ord for JsSymbol {
385    #[inline]
386    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
387        self.hash().cmp(&other.hash())
388    }
389}
390
391impl Hash for JsSymbol {
392    fn hash<H: Hasher>(&self, state: &mut H) {
393        self.hash().hash(state);
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use boa_macros::js_str;
400
401    use crate::{
402        Context, JsObject, JsValue, TestAction, builtins::Json, run_test_actions, value::TryIntoJs,
403    };
404
405    use super::JsSymbol;
406    use std::collections::hash_set::HashSet;
407
408    #[test]
409    fn unique() {
410        let max_loop_iterations = 100;
411        let mut set: HashSet<JsSymbol> = HashSet::new();
412        for _ in 0..max_loop_iterations {
413            let symbol = JsSymbol::new(None);
414            if let Some(symbol) = symbol {
415                assert!(set.insert(symbol), "JsSymbol already exists in the set");
416            } else {
417                panic!("JsSymbol::new() failed when creating up to {max_loop_iterations} symbols");
418            }
419        }
420    }
421
422    #[test]
423    fn hidden_in_enumeration() {
424        let mut context = Context::default();
425        let symbol1 = JsSymbol::new(None).unwrap();
426        let symbol2 = JsSymbol::new(None).unwrap();
427        let test_obj = JsObject::from_proto_and_data(None, ());
428        test_obj
429            .set(symbol1, js_str!("Can't see me"), false, &mut context)
430            .unwrap();
431        test_obj
432            .set(js_str!("visible"), true, false, &mut context)
433            .unwrap();
434        test_obj
435            .set(symbol2, js_str!("Still can't see me"), false, &mut context)
436            .unwrap();
437        let values = test_obj
438            .enumerable_own_property_names(crate::property::PropertyNameKind::Value, &mut context)
439            .expect("Test data should be enumerable");
440        assert!(
441            values.len() == 1,
442            "Test data should have exactly one enumerable value, instead found {}",
443            values.len()
444        );
445    }
446
447    #[test]
448    fn hidden_in_stringify() {
449        let mut context = Context::default();
450        let symbol = JsSymbol::new(None).unwrap();
451        let test_obj = JsObject::with_object_proto(context.intrinsics());
452        test_obj
453            .set(symbol, js_str!("This won't show up"), false, &mut context)
454            .unwrap();
455        let json = test_obj
456            .try_into_js(&mut context)
457            .expect("try_into_js() failed");
458        let json_str = Json::stringify(&JsValue::from(0), &[json], &mut context)
459            .expect("Json::stringify() failed")
460            .as_string()
461            .expect("Json::stringify() did not return string");
462        assert_eq!(js_str!("{}"), json_str);
463    }
464    #[test]
465    fn type_conversions() {
466        run_test_actions([
467            TestAction::assert_eq(
468                r#"
469            let symbol = Symbol("symbol");
470            typeof symbol
471        "#,
472                js_str!("symbol"),
473            ),
474            TestAction::assert(
475                r#"
476            symbol == Object(symbol)
477        "#,
478            ),
479        ]);
480    }
481}