1#![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
43const RESERVED_SYMBOL_HASHES: u64 = 127;
48
49fn get_id() -> Option<u64> {
50 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#[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#[derive(Debug, Clone)]
135pub(crate) struct RawJsSymbol {
136 hash: u64,
137 description: Option<Box<[u16]>>,
139}
140
141#[derive(Trace, Finalize, JsData)]
143#[boa_gc(unsafe_empty_trace)]
146#[allow(clippy::module_name_repetitions)]
147pub struct JsSymbol {
148 repr: Tagged<RawJsSymbol>,
149}
150
151unsafe impl Send for JsSymbol {}
153unsafe 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 repr: Tagged::from_tag($variant.hash() as usize),
163 }
164 }
165 )+
166 };
167}
168
169impl JsSymbol {
170 #[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 repr: unsafe { Tagged::from_ptr(Arc::into_raw(arc).cast_mut()) },
185 })
186 }
187
188 #[inline]
190 #[must_use]
191 pub fn description(&self) -> Option<JsString> {
192 match self.repr.unwrap() {
193 UnwrappedTagged::Ptr(ptr) => {
194 unsafe { ptr.as_ref().description.as_ref().map(|v| js_string!(&**v)) }
197 }
198 UnwrappedTagged::Tag(tag) => {
199 let wk = unsafe { WellKnown::from_tag(tag).unwrap_unchecked() };
202 Some(wk.description())
203 }
204 }
205 }
206
207 #[inline]
212 #[must_use]
213 pub fn fn_name(&self) -> JsString {
214 if let UnwrappedTagged::Tag(tag) = self.repr.unwrap() {
215 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 #[inline]
229 #[must_use]
230 pub fn hash(&self) -> u64 {
231 match self.repr.unwrap() {
232 UnwrappedTagged::Ptr(ptr) => {
233 unsafe { ptr.as_ref().hash }
236 }
237 UnwrappedTagged::Tag(tag) => {
238 unsafe { WellKnown::from_tag(tag).unwrap_unchecked().hash() }
241 }
242 }
243 }
244
245 #[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 #[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 #[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 (async_iterator, WellKnown::AsyncIterator),
291 (has_instance, WellKnown::HasInstance),
293 (is_concat_spreadable, WellKnown::IsConcatSpreadable),
295 (iterator, WellKnown::Iterator),
297 (r#match, WellKnown::Match),
299 (match_all, WellKnown::MatchAll),
301 (replace, WellKnown::Replace),
303 (search, WellKnown::Search),
305 (species, WellKnown::Species),
307 (split, WellKnown::Split),
309 (to_primitive, WellKnown::ToPrimitive),
311 (to_string_tag, WellKnown::ToStringTag),
313 (unscopables, WellKnown::Unscopables),
315 (dispose, WellKnown::Dispose),
317 (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 unsafe {
328 let arc = Arc::from_raw(ptr.as_ptr().cast_const());
329 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 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}