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