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