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}