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}