Skip to main content

boa_engine/object/
jsobject.rs

1//! This module implements the `JsObject` structure.
2//!
3//! The `JsObject` is a garbage collected Object.
4
5use super::{
6    JsPrototype, NativeObject, Object, ObjectData, PrivateName, PropertyMap,
7    internal_methods::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS},
8    shape::RootShape,
9};
10use crate::{
11    Context, JsData, JsResult, JsString, JsSymbol, JsValue,
12    builtins::{
13        array::ARRAY_EXOTIC_INTERNAL_METHODS,
14        array_buffer::{ArrayBuffer, BufferObject, SharedArrayBuffer},
15        object::OrdinaryObject,
16    },
17    context::intrinsics::Intrinsics,
18    error::JsNativeError,
19    js_error, js_string,
20    property::{PropertyDescriptor, PropertyKey},
21    value::PreferredType,
22};
23use boa_gc::{self, Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace};
24use core::ptr::fn_addr_eq;
25use std::collections::HashSet;
26use std::{
27    cell::RefCell,
28    collections::HashMap,
29    error::Error,
30    fmt::{self, Debug, Display},
31    hash::Hash,
32    result::Result as StdResult,
33};
34use thin_vec::ThinVec;
35
36#[cfg(not(feature = "jsvalue-enum"))]
37use boa_gc::GcBox;
38
39#[cfg(not(feature = "jsvalue-enum"))]
40use std::ptr::NonNull;
41
42/// A wrapper type for an immutably borrowed type T.
43pub type Ref<'a, T> = GcRef<'a, T>;
44
45/// A wrapper type for a mutably borrowed type T.
46pub type RefMut<'a, T> = GcRefMut<'a, T>;
47
48pub(crate) type ErasedVTableObject = VTableObject<ErasedObjectData>;
49
50/// An `Object` with inner data set to `ErasedObjectData`.
51pub type ErasedObject = Object<ErasedObjectData>;
52
53/// A erased object data type that must never be used directly.
54#[derive(Debug, Trace, Finalize)]
55pub struct ErasedObjectData {}
56
57impl JsData for ErasedObjectData {}
58
59/// Garbage collected `Object`.
60#[derive(Trace, Finalize)]
61#[boa_gc(unsafe_no_drop)]
62pub struct JsObject<T: NativeObject = ErasedObjectData> {
63    inner: Gc<VTableObject<T>>,
64}
65
66impl<T: NativeObject> Clone for JsObject<T> {
67    fn clone(&self) -> Self {
68        Self {
69            inner: self.inner.clone(),
70        }
71    }
72}
73
74/// A weak handle to a [`JsObject`], from [`JsObject::downgrade`].
75///
76/// Opaque on purpose: the object it points at is a crate-private type, and a
77/// host holding one of these has no business naming it. The only question worth
78/// asking is whether the object is still alive, which [`Self::upgrade`]
79/// answers.
80///
81/// This exists for host-side caches keyed by something other than the object
82/// itself. A DOM binding caches a wrapper per node so `===` and expando
83/// properties behave; holding those strongly makes the cache the sole reason
84/// every wrapper stays alive, so nothing keyed by it can ever be released.
85// No `Debug`: the pointee deliberately has none, because printing an object
86// through its own graph is how you get a stack overflow.
87#[allow(missing_debug_implementations)]
88pub struct WeakJsObject<T: NativeObject + ?Sized = ErasedObjectData>(
89    boa_gc::WeakGc<VTableObject<T>>,
90);
91
92// Hand-written rather than derived: `derive(Clone)` would demand `T: Clone`,
93// which the pointee is not and does not need to be. `Sized` because that is
94// what `WeakGc`'s own `Clone` asks for.
95impl<T: NativeObject> Clone for WeakJsObject<T> {
96    fn clone(&self) -> Self {
97        Self(self.0.clone())
98    }
99}
100
101// Delegated rather than derived: `WeakGc` already implements both, and a
102// derive here would install a second `Drop` alongside the one `Finalize`
103// brings.
104impl<T: NativeObject + ?Sized> Finalize for WeakJsObject<T> {}
105unsafe impl<T: NativeObject + ?Sized> Trace for WeakJsObject<T> {
106    boa_gc::custom_trace!(this, mark, mark(&this.0));
107}
108
109impl<T: NativeObject> WeakJsObject<T> {
110    /// The object, if script can still reach it.
111    #[must_use]
112    #[inline]
113    pub fn upgrade(&self) -> Option<JsObject<T>> {
114        self.0.upgrade().map(|inner| JsObject { inner })
115    }
116}
117
118#[allow(missing_debug_implementations)]
119#[derive(Trace, Finalize)]
120pub(crate) struct VTableObject<T: NativeObject + ?Sized> {
121    #[unsafe_ignore_trace]
122    vtable: &'static InternalObjectMethods,
123    object: GcRefCell<Object<T>>,
124}
125
126impl JsObject {
127    /// Converts the `JsObject` into a raw pointer to its inner `GcBox<ErasedVTableObject>`.
128    #[cfg(not(feature = "jsvalue-enum"))]
129    pub(crate) fn into_raw(self) -> NonNull<GcBox<ErasedVTableObject>> {
130        Gc::into_raw(self.inner)
131    }
132
133    /// Creates a new `JsObject` from a raw pointer.
134    ///
135    /// # Safety
136    /// The caller must ensure that the pointer is valid and points to a `GcBox<ErasedVTableObject>`.
137    /// The pointer must not be null.
138    #[cfg(not(feature = "jsvalue-enum"))]
139    pub(crate) unsafe fn from_raw(raw: NonNull<GcBox<ErasedVTableObject>>) -> Self {
140        // SAFETY: The caller guaranteed the value to be a valid pointer to a `GcBox<ErasedVTableObject>`.
141        let inner = unsafe { Gc::from_raw(raw) };
142
143        JsObject { inner }
144    }
145
146    /// Creates a new ordinary object with its prototype set to the `Object` prototype.
147    ///
148    /// This is an alias for [`Self::with_object_proto`].
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// # use boa_engine::{Context, JsObject};
154    /// let context = &mut Context::default();
155    /// let obj = JsObject::default(context.intrinsics());
156    ///
157    /// assert!(obj.is_ordinary());
158    /// ```
159    #[inline]
160    #[must_use]
161    pub fn default(intrinsics: &Intrinsics) -> Self {
162        Self::with_object_proto(intrinsics)
163    }
164
165    /// Creates a new `JsObject` from its inner object and its vtable.
166    pub(crate) fn from_object_and_vtable<T: NativeObject>(
167        object: Object<T>,
168        vtable: &'static InternalObjectMethods,
169    ) -> Self {
170        let inner = Gc::new(VTableObject {
171            object: GcRefCell::new(object),
172            vtable,
173        });
174
175        JsObject { inner }.upcast()
176    }
177
178    /// Creates a new ordinary object with its prototype set to the `Object` prototype.
179    ///
180    /// This is equivalent to calling the specification's abstract operation
181    /// [`OrdinaryObjectCreate(%Object.prototype%)`][call].
182    ///
183    /// [call]: https://tc39.es/ecma262/#sec-ordinaryobjectcreate
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// # use boa_engine::{Context, JsObject};
189    /// let context = &mut Context::default();
190    /// let obj = JsObject::with_object_proto(context.intrinsics());
191    ///
192    /// assert!(obj.is_ordinary());
193    /// assert!(obj.prototype().is_some());
194    /// ```
195    #[inline]
196    #[must_use]
197    pub fn with_object_proto(intrinsics: &Intrinsics) -> Self {
198        Self::from_proto_and_data(
199            intrinsics.constructors().object().prototype(),
200            OrdinaryObject,
201        )
202    }
203
204    /// Creates a new ordinary object, with its prototype set to null.
205    ///
206    /// This is equivalent to calling the specification's abstract operation
207    /// [`OrdinaryObjectCreate(null)`][call].
208    ///
209    /// [call]: https://tc39.es/ecma262/#sec-ordinaryobjectcreate
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// # use boa_engine::JsObject;
215    /// let obj = JsObject::with_null_proto();
216    /// assert!(obj.prototype().is_none());
217    /// assert!(obj.is_ordinary());
218    /// ```
219    #[inline]
220    #[must_use]
221    pub fn with_null_proto() -> Self {
222        Self::from_proto_and_data(None, OrdinaryObject)
223    }
224
225    /// Creates a new object with the provided prototype and object data.
226    ///
227    /// This is equivalent to calling the specification's abstract operation [`OrdinaryObjectCreate`],
228    /// with the difference that the `additionalInternalSlotsList` parameter is determined by
229    /// the provided `data`.
230    ///
231    /// [`OrdinaryObjectCreate`]: https://tc39.es/ecma262/#sec-ordinaryobjectcreate
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// # use boa_engine::{Context, JsObject};
237    /// # use boa_engine::builtins::object::OrdinaryObject;
238    /// let context = &mut Context::default();
239    /// let obj = JsObject::from_proto_and_data(
240    ///     context.intrinsics().constructors().object().prototype(),
241    ///     OrdinaryObject,
242    /// );
243    ///
244    /// assert!(obj.is_ordinary());
245    /// assert!(obj.prototype().is_some());
246    ///
247    /// // Create an object with no prototype.
248    /// let null_obj = JsObject::from_proto_and_data(None, OrdinaryObject);
249    /// assert!(null_obj.prototype().is_none());
250    /// ```
251    pub fn from_proto_and_data<O: Into<Option<Self>>, T: NativeObject>(
252        prototype: O,
253        data: T,
254    ) -> Self {
255        let internal_methods = data.internal_methods();
256        let inner = Gc::new(VTableObject {
257            object: GcRefCell::new(Object {
258                data: ObjectData::new(data),
259                properties: PropertyMap::from_prototype_unique_shape(prototype.into()),
260                extensible: true,
261                private_elements: ThinVec::new(),
262            }),
263            vtable: internal_methods,
264        });
265
266        JsObject { inner }.upcast()
267    }
268
269    /// Creates a new object with the provided prototype and object data.
270    ///
271    /// This is equivalent to calling the specification's abstract operation [`OrdinaryObjectCreate`],
272    /// with the difference that the `additionalInternalSlotsList` parameter is determined by
273    /// the provided `data`.
274    ///
275    /// [`OrdinaryObjectCreate`]: https://tc39.es/ecma262/#sec-ordinaryobjectcreate
276    pub(crate) fn from_proto_and_data_with_shared_shape<O: Into<Option<Self>>, T: NativeObject>(
277        root_shape: &RootShape,
278        prototype: O,
279        data: T,
280    ) -> JsObject<T> {
281        let internal_methods = data.internal_methods();
282        let inner = Gc::new(VTableObject {
283            object: GcRefCell::new(Object {
284                data: ObjectData::new(data),
285                properties: PropertyMap::from_prototype_with_shared_shape(
286                    root_shape,
287                    prototype.into(),
288                ),
289                extensible: true,
290                private_elements: ThinVec::new(),
291            }),
292            vtable: internal_methods,
293        });
294
295        JsObject { inner }
296    }
297
298    /// Downcasts the object's inner data if the object is of type `T`.
299    ///
300    /// # Panics
301    ///
302    /// Panics if the object is currently mutably borrowed.
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// # use boa_engine::{JsObject, JsData, Trace, Finalize};
308    /// # use boa_engine::builtins::object::OrdinaryObject;
309    /// #[derive(Debug, Trace, Finalize, JsData)]
310    /// struct CustomStruct;
311    ///
312    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
313    ///
314    /// // Downcast consumes the object on success.
315    /// let typed = obj.downcast::<OrdinaryObject>();
316    /// assert!(typed.is_ok());
317    ///
318    /// // Downcast fails for a wrong type, returning the original object.
319    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
320    /// let result = obj.downcast::<CustomStruct>();
321    /// assert!(result.is_err());
322    /// ```
323    pub fn downcast<T: NativeObject>(self) -> Result<JsObject<T>, Self> {
324        if self.is::<T>() {
325            // SAFETY: We have verified that the object is of type `T`, so we can safely cast it.
326            let object = unsafe { self.downcast_unchecked::<T>() };
327
328            Ok(object)
329        } else {
330            Err(self)
331        }
332    }
333
334    /// Downcasts the object's inner data to `T` without verifying the inner type of `T`.
335    ///
336    /// # Safety
337    ///
338    /// For this cast to be sound, `self` must contain an instance of `T` inside its inner data.
339    #[must_use]
340    pub unsafe fn downcast_unchecked<T: NativeObject>(self) -> JsObject<T> {
341        // SAFETY: The caller guarantees `T` is the original inner data type of the underlying
342        // object.
343        // The pointer is guaranteed to be valid because we just created it.
344        // `VTableObject<ErasedObjectData>` and `VTableObject<T>` have the same size and alignment.
345        let inner = unsafe { Gc::cast_unchecked::<VTableObject<T>>(self.inner) };
346
347        JsObject { inner }
348    }
349
350    /// Downcasts a reference to the object,
351    /// if the object is of type `T`.
352    ///
353    /// # Panics
354    ///
355    /// Panics if the object is currently mutably borrowed.
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// # use boa_engine::{JsObject, JsData, Trace, Finalize};
361    /// # use boa_engine::builtins::object::OrdinaryObject;
362    /// #[derive(Debug, Trace, Finalize, JsData)]
363    /// struct CustomStruct;
364    ///
365    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
366    ///
367    /// // Downcast ref succeeds for the correct type.
368    /// assert!(obj.downcast_ref::<OrdinaryObject>().is_some());
369    ///
370    /// // Returns `None` for a wrong type.
371    /// assert!(obj.downcast_ref::<CustomStruct>().is_none());
372    /// ```
373    #[must_use]
374    #[track_caller]
375    pub fn downcast_ref<T: NativeObject>(&self) -> Option<Ref<'_, T>> {
376        if self.is::<T>() {
377            let obj = self.borrow();
378
379            // SAFETY: We have verified that the object is of type `T`, so we can safely cast it.
380            let obj = unsafe { GcRef::cast::<Object<T>>(obj) };
381
382            return Some(Ref::map(obj, |r| r.data()));
383        }
384        None
385    }
386
387    /// Downcasts a mutable reference to the object,
388    /// if the object is type native object type `T`.
389    ///
390    /// # Panics
391    ///
392    /// Panics if the object is currently borrowed.
393    ///
394    /// # Examples
395    ///
396    /// ```
397    /// # use boa_engine::{JsObject, JsData, Trace, Finalize};
398    /// # use boa_engine::builtins::object::OrdinaryObject;
399    /// #[derive(Debug, Trace, Finalize, JsData)]
400    /// struct CustomStruct;
401    ///
402    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
403    ///
404    /// // Downcast mut succeeds for the correct type.
405    /// assert!(obj.downcast_mut::<OrdinaryObject>().is_some());
406    ///
407    /// // Returns `None` for a wrong type.
408    /// assert!(obj.downcast_mut::<CustomStruct>().is_none());
409    /// ```
410    #[must_use]
411    #[track_caller]
412    pub fn downcast_mut<T: NativeObject>(&self) -> Option<RefMut<'_, T>> {
413        if self.is::<T>() {
414            let obj = self.borrow_mut();
415
416            // SAFETY: We have verified that the object is of type `T`, so we can safely cast it.
417            let obj = unsafe { GcRefMut::cast::<Object<T>>(obj) };
418
419            return Some(RefMut::map(obj, |c| c.data_mut()));
420        }
421        None
422    }
423
424    /// Checks if this object is an instance of a certain `NativeObject`.
425    ///
426    /// # Panics
427    ///
428    /// Panics if the object is currently mutably borrowed.
429    ///
430    /// # Examples
431    ///
432    /// ```
433    /// # use boa_engine::{JsObject, JsData, Trace, Finalize};
434    /// # use boa_engine::builtins::object::OrdinaryObject;
435    /// #[derive(Debug, Trace, Finalize, JsData)]
436    /// struct CustomStruct;
437    ///
438    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
439    ///
440    /// assert!(obj.is::<OrdinaryObject>());
441    /// assert!(!obj.is::<CustomStruct>());
442    /// ```
443    #[inline]
444    #[must_use]
445    #[track_caller]
446    pub fn is<T: NativeObject>(&self) -> bool {
447        Gc::is::<VTableObject<T>>(&self.inner)
448    }
449
450    /// Checks if it's an ordinary object.
451    ///
452    /// # Panics
453    ///
454    /// Panics if the object is currently mutably borrowed.
455    ///
456    /// # Examples
457    ///
458    /// ```
459    /// # use boa_engine::{Context, JsObject};
460    /// let context = &mut Context::default();
461    /// let obj = JsObject::with_object_proto(context.intrinsics());
462    ///
463    /// assert!(obj.is_ordinary());
464    /// ```
465    #[inline]
466    #[must_use]
467    #[track_caller]
468    pub fn is_ordinary(&self) -> bool {
469        self.is::<OrdinaryObject>()
470    }
471
472    /// Checks if it's an `Array` object.
473    ///
474    /// # Examples
475    ///
476    /// ```
477    /// # use boa_engine::{Context, JsObject, JsValue ,JsResult};
478    /// # use boa_engine::object::builtins::JsArray;
479    /// # fn main() -> JsResult<()> {
480    /// let context = &mut Context::default();
481    ///
482    /// let array = JsArray::new(context)?;
483    /// // A JsArray's inner JsObject is an array.
484    /// assert!(JsObject::from(array).is_array());
485    ///
486    /// // An ordinary object is not an array.
487    /// let obj = JsObject::with_object_proto(context.intrinsics());
488    /// assert!(!obj.is_array());
489    /// # Ok(())
490    /// # }
491    /// ```
492    #[inline]
493    #[must_use]
494    #[track_caller]
495    pub fn is_array(&self) -> bool {
496        std::ptr::eq(self.vtable(), &raw const ARRAY_EXOTIC_INTERNAL_METHODS)
497    }
498
499    /// The inner implementation of `deep_strict_equals`, which keeps a list of values we've
500    /// seen to avoid recursive objects.
501    pub(crate) fn deep_strict_equals_inner(
502        lhs: &Self,
503        rhs: &Self,
504        encounters: &mut HashSet<usize>,
505        context: &mut Context,
506    ) -> JsResult<bool> {
507        // Loop through all the keys and if one is not equal, return false.
508        fn key_loop(
509            lhs: &JsObject,
510            rhs: &JsObject,
511            encounters: &mut HashSet<usize>,
512            context: &mut Context,
513        ) -> JsResult<bool> {
514            let l_keys = lhs.own_property_keys(context)?;
515            let r_keys = rhs.own_property_keys(context)?;
516
517            if l_keys.len() != r_keys.len() {
518                return Ok(false);
519            }
520
521            for key in &l_keys {
522                let vl = lhs.get_property(key);
523                let vr = rhs.get_property(key);
524
525                match (vl, vr) {
526                    (None, None) => {}
527                    (Some(vl), Some(vr)) => match (vl.value(), vr.value()) {
528                        (None, None) => {}
529                        (Some(lv), Some(rv)) => {
530                            if !lv.deep_strict_equals_inner(rv, encounters, context)? {
531                                return Ok(false);
532                            }
533                        }
534                        _ => {
535                            return Ok(false);
536                        }
537                    },
538                    _ => {
539                        return Ok(false);
540                    }
541                }
542            }
543            Ok(true)
544        }
545
546        let addr_l = std::ptr::from_ref::<Self>(lhs) as usize;
547        let addr_r = std::ptr::from_ref::<Self>(rhs) as usize;
548
549        if addr_r == addr_l {
550            return Ok(true);
551        }
552
553        let contains_l = encounters.contains(&addr_l);
554        let contains_r = encounters.contains(&addr_r);
555        if contains_l || contains_r {
556            return Ok(false);
557        }
558
559        encounters.insert(addr_l);
560        encounters.insert(addr_r);
561
562        // Make sure we clean up after the recursion.
563        let result = key_loop(lhs, rhs, encounters, context);
564        encounters.remove(&addr_l);
565        encounters.remove(&addr_r);
566        result
567    }
568
569    /// Checks that all own property keys and values are equal (recursively).
570    ///
571    /// # Examples
572    ///
573    /// ```
574    /// # use boa_engine::{Context, JsObject, JsResult, js_string};
575    /// # fn main() -> JsResult<()> {
576    /// let context = &mut Context::default();
577    ///
578    /// let obj1 = JsObject::with_object_proto(context.intrinsics());
579    /// obj1.set(js_string!("key"), 42, false, context)?;
580    ///
581    /// let obj2 = JsObject::with_object_proto(context.intrinsics());
582    /// obj2.set(js_string!("key"), 42, false, context)?;
583    ///
584    /// assert!(JsObject::deep_strict_equals(&obj1, &obj2, context)?);
585    /// # Ok(())
586    /// # }
587    /// ```
588    #[inline]
589    pub fn deep_strict_equals(lhs: &Self, rhs: &Self, context: &mut Context) -> JsResult<bool> {
590        Self::deep_strict_equals_inner(lhs, rhs, &mut HashSet::new(), context)
591    }
592
593    /// The abstract operation `ToPrimitive` takes an input argument and an optional argument
594    /// `PreferredType`.
595    ///
596    /// <https://tc39.es/ecma262/#sec-toprimitive>
597    pub fn to_primitive(
598        &self,
599        context: &mut Context,
600        preferred_type: PreferredType,
601    ) -> JsResult<JsValue> {
602        // a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
603        let Some(exotic_to_prim) = self.get_method(JsSymbol::to_primitive(), context)? else {
604            // c. If preferredType is not present, let preferredType be number.
605            let preferred_type = match preferred_type {
606                PreferredType::Default | PreferredType::Number => PreferredType::Number,
607                PreferredType::String => PreferredType::String,
608            };
609            return self.ordinary_to_primitive(context, preferred_type);
610        };
611
612        // b. If exoticToPrim is not undefined, then
613        //    i. If preferredType is not present, let hint be "default".
614        //    ii. Else if preferredType is string, let hint be "string".
615        //    iii. Else,
616        //        1. Assert: preferredType is number.
617        //        2. Let hint be "number".
618        let hint = match preferred_type {
619            PreferredType::Default => js_string!("default"),
620            PreferredType::String => js_string!("string"),
621            PreferredType::Number => js_string!("number"),
622        }
623        .into();
624
625        //    iv. Let result be ? Call(exoticToPrim, input, « hint »).
626        let result = exotic_to_prim.call(&self.clone().into(), &[hint], context)?;
627
628        //    v. If Type(result) is not Object, return result.
629        //    vi. Throw a TypeError exception.
630        if result.is_object() {
631            Err(js_error!(
632                TypeError: "method `[Symbol.toPrimitive]` cannot return an object"
633            ))
634        } else {
635            Ok(result)
636        }
637    }
638
639    /// Converts an object to a primitive.
640    ///
641    /// Diverges from the spec to prevent a stack overflow when the object is recursive.
642    /// For example,
643    /// ```javascript
644    /// let a = [1];
645    /// a[1] = a;
646    /// console.log(a.toString()); // We print "1,"
647    /// ```
648    /// The spec doesn't mention what to do in this situation, but a naive implementation
649    /// would overflow the stack recursively calling `toString()`. We follow v8 and SpiderMonkey
650    /// instead by returning a default value for the given `hint` -- either `0.` or `""`.
651    /// Example in v8: <https://repl.it/repls/IvoryCircularCertification#index.js>
652    ///
653    /// More information:
654    ///  - [ECMAScript][spec]
655    ///
656    /// [spec]: https://tc39.es/ecma262/#sec-ordinarytoprimitive
657    pub(crate) fn ordinary_to_primitive(
658        &self,
659        context: &mut Context,
660        hint: PreferredType,
661    ) -> JsResult<JsValue> {
662        // 1. Assert: Type(O) is Object.
663        //      Already is JsObject by type.
664        // 2. Assert: Type(hint) is String and its value is either "string" or "number".
665        debug_assert!(hint == PreferredType::String || hint == PreferredType::Number);
666
667        // Diverge from the spec here to make sure we aren't going to overflow the stack by converting
668        // a recursive structure
669        // We can follow v8 & SpiderMonkey's lead and return a default value for the hint in this situation
670        // (see https://repl.it/repls/IvoryCircularCertification#index.js)
671        let recursion_limiter = RecursionLimiter::new(self.as_ref());
672        if recursion_limiter.live {
673            // we're in a recursive object, bail
674            return Ok(match hint {
675                PreferredType::Number => JsValue::new(0),
676                PreferredType::String => JsValue::new(js_string!()),
677                PreferredType::Default => unreachable!("checked type hint in step 2"),
678            });
679        }
680
681        // 3. If hint is "string", then
682        //    a. Let methodNames be « "toString", "valueOf" ».
683        // 4. Else,
684        //    a. Let methodNames be « "valueOf", "toString" ».
685        let method_names = if hint == PreferredType::String {
686            [js_string!("toString"), js_string!("valueOf")]
687        } else {
688            [js_string!("valueOf"), js_string!("toString")]
689        };
690
691        // 5. For each name in methodNames in List order, do
692        for name in method_names {
693            // a. Let method be ? Get(O, name).
694            let method = self.get(name, context)?;
695
696            // b. If IsCallable(method) is true, then
697            if let Some(method) = method.as_callable() {
698                // i. Let result be ? Call(method, O).
699                let result = method.call(&self.clone().into(), &[], context)?;
700
701                // ii. If Type(result) is not Object, return result.
702                if !result.is_object() {
703                    return Ok(result);
704                }
705            }
706        }
707
708        // 6. Throw a TypeError exception.
709        Err(JsNativeError::typ()
710            .with_message("cannot convert object to primitive value")
711            .into())
712    }
713
714    /// The abstract operation `ToPropertyDescriptor`.
715    ///
716    /// More information:
717    /// - [ECMAScript reference][spec]
718    ///
719    /// [spec]: https://tc39.es/ecma262/#sec-topropertydescriptor
720    pub fn to_property_descriptor(&self, context: &mut Context) -> JsResult<PropertyDescriptor> {
721        // 1 is implemented on the method `to_property_descriptor` of value
722
723        // 2. Let desc be a new Property Descriptor that initially has no fields.
724        let mut desc = PropertyDescriptor::builder();
725
726        // 3. Let hasEnumerable be ? HasProperty(Obj, "enumerable").
727        // 4. If hasEnumerable is true, then ...
728        if let Some(enumerable) = self.try_get(js_string!("enumerable"), context)? {
729            // a. Let enumerable be ! ToBoolean(? Get(Obj, "enumerable")).
730            // b. Set desc.[[Enumerable]] to enumerable.
731            desc = desc.enumerable(enumerable.to_boolean());
732        }
733
734        // 5. Let hasConfigurable be ? HasProperty(Obj, "configurable").
735        // 6. If hasConfigurable is true, then ...
736        if let Some(configurable) = self.try_get(js_string!("configurable"), context)? {
737            // a. Let configurable be ! ToBoolean(? Get(Obj, "configurable")).
738            // b. Set desc.[[Configurable]] to configurable.
739            desc = desc.configurable(configurable.to_boolean());
740        }
741
742        // 7. Let hasValue be ? HasProperty(Obj, "value").
743        // 8. If hasValue is true, then ...
744        if let Some(value) = self.try_get(js_string!("value"), context)? {
745            // a. Let value be ? Get(Obj, "value").
746            // b. Set desc.[[Value]] to value.
747            desc = desc.value(value);
748        }
749
750        // 9. Let hasWritable be ? HasProperty(Obj, ).
751        // 10. If hasWritable is true, then ...
752        if let Some(writable) = self.try_get(js_string!("writable"), context)? {
753            // a. Let writable be ! ToBoolean(? Get(Obj, "writable")).
754            // b. Set desc.[[Writable]] to writable.
755            desc = desc.writable(writable.to_boolean());
756        }
757
758        // 11. Let hasGet be ? HasProperty(Obj, "get").
759        // 12. If hasGet is true, then
760        // 12.a. Let getter be ? Get(Obj, "get").
761        let get = if let Some(getter) = self.try_get(js_string!("get"), context)? {
762            // b. If IsCallable(getter) is false and getter is not undefined, throw a TypeError exception.
763            // todo: extract IsCallable to be callable from Value
764            if !getter.is_undefined() && getter.as_object().is_none_or(|o| !o.is_callable()) {
765                return Err(JsNativeError::typ()
766                    .with_message("Property descriptor getter must be callable")
767                    .into());
768            }
769            // c. Set desc.[[Get]] to getter.
770            Some(getter)
771        } else {
772            None
773        };
774
775        // 13. Let hasSet be ? HasProperty(Obj, "set").
776        // 14. If hasSet is true, then
777        // 14.a. Let setter be ? Get(Obj, "set").
778        let set = if let Some(setter) = self.try_get(js_string!("set"), context)? {
779            // 14.b. If IsCallable(setter) is false and setter is not undefined, throw a TypeError exception.
780            // todo: extract IsCallable to be callable from Value
781            if !setter.is_undefined() && setter.as_object().is_none_or(|o| !o.is_callable()) {
782                return Err(JsNativeError::typ()
783                    .with_message("Property descriptor setter must be callable")
784                    .into());
785            }
786            // 14.c. Set desc.[[Set]] to setter.
787            Some(setter)
788        } else {
789            None
790        };
791
792        // 15. If desc.[[Get]] is present or desc.[[Set]] is present, then ...
793        // a. If desc.[[Value]] is present or desc.[[Writable]] is present, throw a TypeError exception.
794        if get.as_ref().or(set.as_ref()).is_some() && desc.inner().is_data_descriptor() {
795            return Err(JsNativeError::typ()
796                .with_message(
797                    "Invalid property descriptor.\
798Cannot both specify accessors and a value or writable attribute",
799                )
800                .into());
801        }
802
803        desc = desc.maybe_get(get).maybe_set(set);
804
805        // 16. Return desc.
806        Ok(desc.build())
807    }
808
809    // Allow lint, false positive.
810    #[allow(clippy::assigning_clones)]
811    pub(crate) fn get_property(&self, key: &PropertyKey) -> Option<PropertyDescriptor> {
812        let mut obj = Some(self.clone());
813
814        while let Some(o) = obj {
815            if let Some(v) = o.borrow().properties.get(key) {
816                return Some(v);
817            }
818            obj = o.borrow().prototype().clone();
819        }
820        None
821    }
822
823    /// Casts to a `BufferObject` if the object is an `ArrayBuffer` or a `SharedArrayBuffer`.
824    #[inline]
825    pub(crate) fn into_buffer_object(self) -> Result<BufferObject, JsObject> {
826        match self.downcast::<ArrayBuffer>() {
827            Ok(buffer) => Ok(BufferObject::Buffer(buffer)),
828            Err(object) => object
829                .downcast::<SharedArrayBuffer>()
830                .map(BufferObject::SharedBuffer),
831        }
832    }
833}
834
835impl<T: NativeObject> JsObject<T> {
836    /// A weak handle to this object.
837    ///
838    /// The counterpart to [`WeakGc::upgrade`], and the reason it is needed: a
839    /// host that caches wrappers by some native key has to be able to ask
840    /// whether script can still reach one. Holding the strong handle makes the
841    /// cache itself the answer, so the cache pins every object it has ever seen
842    /// and nothing keyed by it can ever be released.
843    ///
844    /// A DOM binding is the motivating case. Wrappers are cached per node so
845    /// that `===` and expando properties behave, and with a strong cache the
846    /// document can never drop a removed node, because every node script has
847    /// touched looks reachable for ever.
848    #[must_use]
849    #[inline]
850    pub fn downgrade(&self) -> WeakJsObject<T> {
851        WeakJsObject(boa_gc::WeakGc::new(&self.inner))
852    }
853
854    /// Immutably borrows the `Object`.
855    ///
856    /// The borrow lasts until the returned `Ref` exits scope.
857    /// Multiple immutable borrows can be taken out at the same time.
858    ///
859    /// # Panics
860    ///
861    /// Panics if the object is currently mutably borrowed.
862    ///
863    /// # Examples
864    ///
865    /// ```
866    /// # use boa_engine::JsObject;
867    /// # use boa_engine::builtins::object::OrdinaryObject;
868    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
869    ///
870    /// // Multiple immutable borrows are allowed.
871    /// let borrowed = obj.borrow();
872    /// assert!(borrowed.prototype().is_none());
873    /// ```
874    #[inline]
875    #[must_use]
876    #[track_caller]
877    pub fn borrow(&self) -> Ref<'_, Object<T>> {
878        self.try_borrow().expect("Object already mutably borrowed")
879    }
880
881    /// Mutably borrows the Object.
882    ///
883    /// The borrow lasts until the returned `RefMut` exits scope.
884    /// The object cannot be borrowed while this borrow is active.
885    ///
886    /// # Panics
887    /// Panics if the object is currently borrowed.
888    ///
889    /// # Examples
890    ///
891    /// ```
892    /// # use boa_engine::{Context, JsObject};
893    /// # use boa_engine::builtins::object::OrdinaryObject;
894    /// let context = &mut Context::default();
895    /// let obj = JsObject::from_proto_and_data(
896    ///     context.intrinsics().constructors().object().prototype(),
897    ///     OrdinaryObject,
898    /// );
899    ///
900    /// // Set the prototype to `None` via a mutable borrow.
901    /// obj.borrow_mut().set_prototype(None);
902    /// assert!(obj.prototype().is_none());
903    /// ```
904    #[inline]
905    #[must_use]
906    #[track_caller]
907    pub fn borrow_mut(&self) -> RefMut<'_, Object<T>> {
908        self.try_borrow_mut().expect("Object already borrowed")
909    }
910
911    /// Immutably borrows the `Object`, returning an error if the value is currently mutably borrowed.
912    ///
913    /// The borrow lasts until the returned `GcCellRef` exits scope.
914    /// Multiple immutable borrows can be taken out at the same time.
915    ///
916    /// This is the non-panicking variant of [`borrow`](#method.borrow).
917    ///
918    /// # Examples
919    ///
920    /// ```
921    /// # use boa_engine::JsObject;
922    /// # use boa_engine::builtins::object::OrdinaryObject;
923    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
924    ///
925    /// // Non-panicking immutable borrow.
926    /// let result = obj.try_borrow();
927    /// assert!(result.is_ok());
928    /// ```
929    #[inline]
930    pub fn try_borrow(&self) -> StdResult<Ref<'_, Object<T>>, BorrowError> {
931        self.inner.object.try_borrow().map_err(|_| BorrowError)
932    }
933
934    /// Mutably borrows the object, returning an error if the value is currently borrowed.
935    ///
936    /// The borrow lasts until the returned `GcCellRefMut` exits scope.
937    /// The object be borrowed while this borrow is active.
938    ///
939    /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut).
940    ///
941    /// # Examples
942    ///
943    /// ```
944    /// # use boa_engine::JsObject;
945    /// # use boa_engine::builtins::object::OrdinaryObject;
946    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
947    ///
948    /// // Non-panicking mutable borrow.
949    /// let result = obj.try_borrow_mut();
950    /// assert!(result.is_ok());
951    /// ```
952    #[inline]
953    pub fn try_borrow_mut(&self) -> StdResult<RefMut<'_, Object<T>>, BorrowMutError> {
954        self.inner
955            .object
956            .try_borrow_mut()
957            .map_err(|_| BorrowMutError)
958    }
959
960    /// Checks if the garbage collected memory is the same.
961    ///
962    /// # Examples
963    ///
964    /// ```
965    /// # use boa_engine::JsObject;
966    /// # use boa_engine::builtins::object::OrdinaryObject;
967    /// let obj = JsObject::from_proto_and_data(None, OrdinaryObject);
968    /// let clone = obj.clone();
969    ///
970    /// // A clone points to the same GC allocation.
971    /// assert!(JsObject::equals(&obj, &clone));
972    ///
973    /// // A separate object is different, even with identical data.
974    /// let other = JsObject::from_proto_and_data(None, OrdinaryObject);
975    /// assert!(!JsObject::equals(&obj, &other));
976    /// ```
977    #[must_use]
978    #[inline]
979    pub fn equals(lhs: &Self, rhs: &Self) -> bool {
980        Gc::ptr_eq(lhs.inner(), rhs.inner())
981    }
982
983    /// Get the prototype of the object.
984    ///
985    /// # Panics
986    ///
987    /// Panics if the object is currently mutably borrowed.
988    ///
989    /// # Examples
990    ///
991    /// ```
992    /// # use boa_engine::{Context, JsObject};
993    /// let context = &mut Context::default();
994    ///
995    /// let obj = JsObject::with_object_proto(context.intrinsics());
996    /// assert!(obj.prototype().is_some());
997    ///
998    /// let null_obj = JsObject::with_null_proto();
999    /// assert!(null_obj.prototype().is_none());
1000    /// ```
1001    #[inline]
1002    #[must_use]
1003    #[track_caller]
1004    pub fn prototype(&self) -> JsPrototype {
1005        self.borrow().prototype()
1006    }
1007
1008    /// Get the extensibility of the object.
1009    ///
1010    /// # Panics
1011    ///
1012    /// Panics if the object is currently mutably borrowed.
1013    pub(crate) fn extensible(&self) -> bool {
1014        self.borrow().extensible
1015    }
1016
1017    /// Set the prototype of the object.
1018    ///
1019    /// # Panics
1020    ///
1021    /// Panics if the object is currently mutably borrowed
1022    ///
1023    /// # Examples
1024    ///
1025    /// ```
1026    /// # use boa_engine::{Context, JsObject};
1027    /// let context = &mut Context::default();
1028    /// let obj = JsObject::with_object_proto(context.intrinsics());
1029    ///
1030    /// assert!(obj.prototype().is_some());
1031    ///
1032    /// // Set the prototype to `None`.
1033    /// obj.set_prototype(None);
1034    /// assert!(obj.prototype().is_none());
1035    /// ```
1036    #[inline]
1037    #[track_caller]
1038    #[allow(clippy::must_use_candidate)]
1039    pub fn set_prototype(&self, prototype: JsPrototype) -> bool {
1040        self.borrow_mut().set_prototype(prototype)
1041    }
1042
1043    /// Helper function for property insertion.
1044    #[track_caller]
1045    pub(crate) fn insert<K, P>(&self, key: K, property: P) -> bool
1046    where
1047        K: Into<PropertyKey>,
1048        P: Into<PropertyDescriptor>,
1049    {
1050        self.borrow_mut().insert(key, property)
1051    }
1052
1053    /// Inserts a field in the object `properties` without checking if it's writable.
1054    ///
1055    /// If a field was already in the object with the same name, than `true` is returned
1056    /// with that field, otherwise `false` is returned.
1057    pub fn insert_property<K, P>(&self, key: K, property: P) -> bool
1058    where
1059        K: Into<PropertyKey>,
1060        P: Into<PropertyDescriptor>,
1061    {
1062        self.insert(key.into(), property)
1063    }
1064
1065    /// It determines if Object is a callable function with a `[[Call]]` internal method.
1066    ///
1067    /// More information:
1068    /// - [ECMAScript reference][spec]
1069    ///
1070    /// [spec]: https://tc39.es/ecma262/#sec-iscallable
1071    #[inline]
1072    #[must_use]
1073    pub fn is_callable(&self) -> bool {
1074        !fn_addr_eq(
1075            self.inner.vtable.__call__,
1076            ORDINARY_INTERNAL_METHODS.__call__,
1077        )
1078    }
1079
1080    /// It determines if Object is a function object with a `[[Construct]]` internal method.
1081    ///
1082    /// More information:
1083    /// - [ECMAScript reference][spec]
1084    ///
1085    /// [spec]: https://tc39.es/ecma262/#sec-isconstructor
1086    #[inline]
1087    #[must_use]
1088    pub fn is_constructor(&self) -> bool {
1089        !fn_addr_eq(
1090            self.inner.vtable.__construct__,
1091            ORDINARY_INTERNAL_METHODS.__construct__,
1092        )
1093    }
1094
1095    pub(crate) fn vtable(&self) -> &'static InternalObjectMethods {
1096        self.inner.vtable
1097    }
1098
1099    pub(crate) fn inner(&self) -> &Gc<VTableObject<T>> {
1100        &self.inner
1101    }
1102
1103    pub(crate) fn from_inner(inner: Gc<VTableObject<T>>) -> Self {
1104        Self { inner }
1105    }
1106
1107    /// Create a new private name with this object as the unique identifier.
1108    pub(crate) fn private_name(&self, description: JsString) -> PrivateName {
1109        let ptr: *const _ = self.as_ref();
1110        PrivateName::new(description, ptr.cast::<()>() as usize)
1111    }
1112}
1113
1114impl<T: NativeObject> JsObject<T> {
1115    /// Creates a new `JsObject` from its root shape, prototype, and data.
1116    ///
1117    /// Note that the returned object will not be erased to be convertible to a
1118    /// `JsValue`. To erase the pointer, call [`JsObject::upcast`].
1119    ///
1120    /// # Examples
1121    ///
1122    /// ```
1123    /// # use boa_engine::{Context, JsObject};
1124    /// # use boa_engine::builtins::object::OrdinaryObject;
1125    /// let context = &mut Context::default();
1126    ///
1127    /// let typed_obj = JsObject::new(
1128    ///     context.root_shape(),
1129    ///     context.intrinsics().constructors().object().prototype(),
1130    ///     OrdinaryObject,
1131    /// );
1132    ///
1133    /// // Upcast to an erased JsObject to use with JsValue.
1134    /// let obj = typed_obj.upcast();
1135    /// assert!(obj.is_ordinary());
1136    /// ```
1137    pub fn new<O: Into<Option<JsObject>>>(root_shape: &RootShape, prototype: O, data: T) -> Self {
1138        let internal_methods = data.internal_methods();
1139        let inner = Gc::new(VTableObject {
1140            object: GcRefCell::new(Object {
1141                data: ObjectData::new(data),
1142                properties: PropertyMap::from_prototype_with_shared_shape(
1143                    root_shape,
1144                    prototype.into(),
1145                ),
1146                extensible: true,
1147                private_elements: ThinVec::new(),
1148            }),
1149            vtable: internal_methods,
1150        });
1151
1152        Self { inner }
1153    }
1154
1155    /// Creates a new `JsObject` from prototype, and data.
1156    ///
1157    /// Note that the returned object will not be erased to be convertible to a
1158    /// `JsValue`. To erase the pointer, call [`JsObject::upcast`].
1159    ///
1160    /// # Examples
1161    ///
1162    /// ```
1163    /// # use boa_engine::JsObject;
1164    /// # use boa_engine::builtins::object::OrdinaryObject;
1165    /// let typed_obj = JsObject::new_unique(None, OrdinaryObject);
1166    ///
1167    /// // Upcast to an erased JsObject.
1168    /// let obj = typed_obj.upcast();
1169    /// assert!(obj.is_ordinary());
1170    /// assert!(obj.prototype().is_none());
1171    /// ```
1172    pub fn new_unique<O: Into<Option<JsObject>>>(prototype: O, data: T) -> Self {
1173        let internal_methods = data.internal_methods();
1174        let inner = Gc::new(VTableObject {
1175            object: GcRefCell::new(Object {
1176                data: ObjectData::new(data),
1177                properties: PropertyMap::from_prototype_unique_shape(prototype.into()),
1178                extensible: true,
1179                private_elements: ThinVec::new(),
1180            }),
1181            vtable: internal_methods,
1182        });
1183
1184        Self { inner }
1185    }
1186
1187    /// Upcasts this object's inner data from a specific type `T` to an erased type
1188    /// `dyn NativeObject`.
1189    ///
1190    /// # Examples
1191    ///
1192    /// ```
1193    /// # use boa_engine::JsObject;
1194    /// # use boa_engine::builtins::object::OrdinaryObject;
1195    /// // Create a typed JsObject<OrdinaryObject>.
1196    /// let typed_obj = JsObject::new_unique(None, OrdinaryObject);
1197    ///
1198    /// // Upcast erases the type, producing an untyped JsObject.
1199    /// let obj: JsObject = typed_obj.upcast();
1200    /// assert!(obj.is_ordinary());
1201    /// ```
1202    #[must_use]
1203    pub fn upcast(self) -> JsObject {
1204        // SAFETY: The pointer is guaranteed to be valid.
1205        // `VTableObject<ErasedObjectData>` and `VTableObject<T>` have the same size and alignment.
1206        let inner = unsafe { Gc::cast_unchecked::<ErasedVTableObject>(self.inner) };
1207
1208        JsObject { inner }
1209    }
1210}
1211
1212impl<T: NativeObject> AsRef<GcRefCell<Object<T>>> for JsObject<T> {
1213    #[inline]
1214    fn as_ref(&self) -> &GcRefCell<Object<T>> {
1215        &self.inner.object
1216    }
1217}
1218
1219impl<T: NativeObject> From<Gc<VTableObject<T>>> for JsObject<T> {
1220    #[inline]
1221    fn from(inner: Gc<VTableObject<T>>) -> Self {
1222        Self { inner }
1223    }
1224}
1225
1226impl<T: NativeObject> PartialEq for JsObject<T> {
1227    fn eq(&self, other: &Self) -> bool {
1228        Self::equals(self, other)
1229    }
1230}
1231
1232impl<T: NativeObject> Eq for JsObject<T> {}
1233
1234impl<T: NativeObject> Hash for JsObject<T> {
1235    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1236        std::ptr::hash(self.as_ref(), state);
1237    }
1238}
1239
1240/// An error returned by [`JsObject::try_borrow`](struct.JsObject.html#method.try_borrow).
1241#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1242pub struct BorrowError;
1243
1244impl Display for BorrowError {
1245    #[inline]
1246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247        Display::fmt("Object already mutably borrowed", f)
1248    }
1249}
1250
1251impl Error for BorrowError {}
1252
1253/// An error returned by [`JsObject::try_borrow_mut`](struct.JsObject.html#method.try_borrow_mut).
1254#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1255pub struct BorrowMutError;
1256
1257impl Display for BorrowMutError {
1258    #[inline]
1259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1260        Display::fmt("Object already borrowed", f)
1261    }
1262}
1263
1264impl Error for BorrowMutError {}
1265
1266#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1267enum RecursionValueState {
1268    /// This value is "live": there's an active `RecursionLimiter` that hasn't been dropped.
1269    Live,
1270    /// This value has been seen before, but the recursion limiter has been dropped.
1271    /// For example:
1272    /// ```javascript
1273    /// let b = [];
1274    /// JSON.stringify([ // Create a recursion limiter for the root here
1275    ///    b,            // state for b's &JsObject here is None
1276    ///    b,            // state for b's &JsObject here is Visited
1277    /// ]);
1278    /// ```
1279    Visited,
1280}
1281
1282/// Prevents infinite recursion during `Debug::fmt`, `JSON.stringify`, and other conversions.
1283/// This uses a thread local, so is not safe to use where the object graph will be traversed by
1284/// multiple threads!
1285#[derive(Debug)]
1286pub struct RecursionLimiter {
1287    /// If this was the first `JsObject` in the tree.
1288    top_level: bool,
1289    /// The ptr being kept in the `HashSet`, so we can delete it when we drop.
1290    ptr: usize,
1291    /// If this `JsObject` has been visited before in the graph, but not in the current branch.
1292    pub visited: bool,
1293    /// If this `JsObject` has been visited in the current branch of the graph.
1294    pub live: bool,
1295}
1296
1297impl Drop for RecursionLimiter {
1298    fn drop(&mut self) {
1299        if self.top_level {
1300            // When the top level of the graph is dropped, we can free the entire map for the next traversal.
1301            SEEN.with(|hm| hm.borrow_mut().clear());
1302        } else if !self.live {
1303            // This was the first RL for this object to become live, so it's no longer live now that it's dropped.
1304            SEEN.with(|hm| {
1305                hm.borrow_mut()
1306                    .insert(self.ptr, RecursionValueState::Visited)
1307            });
1308        }
1309    }
1310}
1311
1312thread_local! {
1313    /// The map of pointers to `JsObject` that have been visited during the current `Debug::fmt` graph,
1314    /// and the current state of their RecursionLimiter (dropped or live -- see `RecursionValueState`)
1315    static SEEN: RefCell<HashMap<usize, RecursionValueState>> = RefCell::new(HashMap::new());
1316}
1317
1318impl RecursionLimiter {
1319    /// Determines if the specified `T` has been visited, and returns a struct that will free it when dropped.
1320    ///
1321    /// This is done by maintaining a thread-local hashset containing the pointers of `T` values that have been
1322    /// visited. The first `T` visited will clear the hashset, while any others will check if they are contained
1323    /// by the hashset.
1324    pub fn new<T: ?Sized>(o: &T) -> Self {
1325        let ptr: *const _ = o;
1326        let ptr = ptr.cast::<()>() as usize;
1327        let (top_level, visited, live) = SEEN.with(|hm| {
1328            let mut hm = hm.borrow_mut();
1329            let top_level = hm.is_empty();
1330            let old_state = hm.insert(ptr, RecursionValueState::Live);
1331
1332            (
1333                top_level,
1334                old_state == Some(RecursionValueState::Visited),
1335                old_state == Some(RecursionValueState::Live),
1336            )
1337        });
1338
1339        Self {
1340            top_level,
1341            ptr,
1342            visited,
1343            live,
1344        }
1345    }
1346}
1347
1348impl<T: NativeObject> Debug for JsObject<T> {
1349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1350        let limiter = RecursionLimiter::new(self.as_ref());
1351
1352        // Typically, using `!limiter.live` would be good enough here.
1353        // However, the JS object hierarchy involves quite a bit of repetition, and the sheer amount of data makes
1354        // understanding the Debug output impossible; limiting the usefulness of it.
1355        //
1356        // Instead, we check if the object has appeared before in the entire graph. This means that objects will appear
1357        // at most once, hopefully making things a bit clearer.
1358        if !limiter.visited && !limiter.live {
1359            let ptr: *const _ = self.as_ref();
1360            let ptr = ptr.cast::<()>();
1361            let obj = self.borrow();
1362            let kind = obj.data().type_name_of_value();
1363            if self.is_callable() {
1364                let name_prop = obj
1365                    .properties()
1366                    .get(&PropertyKey::String(js_string!("name")));
1367                let name = match name_prop {
1368                    None => JsString::default(),
1369                    Some(prop) => prop
1370                        .value()
1371                        .and_then(JsValue::as_string)
1372                        .unwrap_or_default(),
1373                };
1374
1375                return f.write_fmt(format_args!("({:?}) {:?} 0x{:X}", kind, name, ptr as usize));
1376            }
1377
1378            f.write_fmt(format_args!("({:?}) 0x{:X}", kind, ptr as usize))
1379        } else {
1380            f.write_str("{ ... }")
1381        }
1382    }
1383}