use super::{
JsPrototype, NativeObject, Object, ObjectData, PrivateName, PropertyMap,
internal_methods::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS},
shape::RootShape,
};
use crate::{
Context, JsData, JsResult, JsString, JsSymbol, JsValue,
builtins::{
array::ARRAY_EXOTIC_INTERNAL_METHODS,
array_buffer::{ArrayBuffer, BufferObject, SharedArrayBuffer},
object::OrdinaryObject,
},
context::intrinsics::Intrinsics,
error::JsNativeError,
js_error, js_string,
property::{PropertyDescriptor, PropertyKey},
value::PreferredType,
};
use boa_gc::{self, Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace};
use core::ptr::fn_addr_eq;
use std::collections::HashSet;
use std::{
cell::RefCell,
collections::HashMap,
error::Error,
fmt::{self, Debug, Display},
hash::Hash,
result::Result as StdResult,
};
use thin_vec::ThinVec;
#[cfg(not(feature = "jsvalue-enum"))]
use boa_gc::GcBox;
#[cfg(not(feature = "jsvalue-enum"))]
use std::ptr::NonNull;
pub type Ref<'a, T> = GcRef<'a, T>;
pub type RefMut<'a, T> = GcRefMut<'a, T>;
pub(crate) type ErasedVTableObject = VTableObject<ErasedObjectData>;
pub type ErasedObject = Object<ErasedObjectData>;
#[derive(Debug, Trace, Finalize)]
pub struct ErasedObjectData {}
impl JsData for ErasedObjectData {}
#[derive(Trace, Finalize)]
#[boa_gc(unsafe_no_drop)]
pub struct JsObject<T: NativeObject = ErasedObjectData> {
inner: Gc<VTableObject<T>>,
}
impl<T: NativeObject> JsObject<T> {
#[inline]
pub(crate) unsafe fn release_outside_collection(self) {
unsafe { Gc::release_outside_collection(self.inner) };
}
}
impl<T: NativeObject> Clone for JsObject<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
#[allow(missing_debug_implementations)]
pub struct WeakJsObject<T: NativeObject + ?Sized = ErasedObjectData>(
boa_gc::WeakGc<VTableObject<T>>,
);
impl<T: NativeObject> Clone for WeakJsObject<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: NativeObject + ?Sized> Finalize for WeakJsObject<T> {}
unsafe impl<T: NativeObject + ?Sized> Trace for WeakJsObject<T> {
boa_gc::custom_trace!(this, mark, mark(&this.0));
}
impl<T: NativeObject> WeakJsObject<T> {
#[must_use]
#[inline]
pub fn upgrade(&self) -> Option<JsObject<T>> {
self.0.upgrade().map(|inner| JsObject { inner })
}
}
#[allow(missing_debug_implementations)]
#[derive(Trace, Finalize)]
pub(crate) struct VTableObject<T: NativeObject + ?Sized> {
#[unsafe_ignore_trace]
vtable: &'static InternalObjectMethods,
object: GcRefCell<Object<T>>,
}
impl JsObject {
#[cfg(not(feature = "jsvalue-enum"))]
pub(crate) fn into_raw(self) -> NonNull<GcBox<ErasedVTableObject>> {
Gc::into_raw(self.inner)
}
#[cfg(not(feature = "jsvalue-enum"))]
pub(crate) unsafe fn from_raw(raw: NonNull<GcBox<ErasedVTableObject>>) -> Self {
let inner = unsafe { Gc::from_raw(raw) };
JsObject { inner }
}
#[inline]
#[must_use]
pub fn default(intrinsics: &Intrinsics) -> Self {
Self::with_object_proto(intrinsics)
}
pub(crate) fn from_object_and_vtable<T: NativeObject>(
object: Object<T>,
vtable: &'static InternalObjectMethods,
) -> Self {
let inner = Gc::new(VTableObject {
object: GcRefCell::new(object),
vtable,
});
JsObject { inner }.upcast()
}
#[inline]
#[must_use]
pub fn with_object_proto(intrinsics: &Intrinsics) -> Self {
Self::from_proto_and_data(
intrinsics.constructors().object().prototype(),
OrdinaryObject,
)
}
#[inline]
#[must_use]
pub fn with_null_proto() -> Self {
Self::from_proto_and_data(None, OrdinaryObject)
}
pub fn from_proto_and_data<O: Into<Option<Self>>, T: NativeObject>(
prototype: O,
data: T,
) -> Self {
let internal_methods = data.internal_methods();
let inner = Gc::new(VTableObject {
object: GcRefCell::new(Object {
data: ObjectData::new(data),
properties: PropertyMap::from_prototype_unique_shape(prototype.into()),
extensible: true,
private_elements: ThinVec::new(),
}),
vtable: internal_methods,
});
JsObject { inner }.upcast()
}
pub(crate) fn from_proto_and_data_with_shared_shape<O: Into<Option<Self>>, T: NativeObject>(
root_shape: &RootShape,
prototype: O,
data: T,
) -> JsObject<T> {
let internal_methods = data.internal_methods();
let inner = Gc::new(VTableObject {
object: GcRefCell::new(Object {
data: ObjectData::new(data),
properties: PropertyMap::from_prototype_with_shared_shape(
root_shape,
prototype.into(),
),
extensible: true,
private_elements: ThinVec::new(),
}),
vtable: internal_methods,
});
JsObject { inner }
}
pub fn downcast<T: NativeObject>(self) -> Result<JsObject<T>, Self> {
if self.is::<T>() {
let object = unsafe { self.downcast_unchecked::<T>() };
Ok(object)
} else {
Err(self)
}
}
#[must_use]
pub unsafe fn downcast_unchecked<T: NativeObject>(self) -> JsObject<T> {
let inner = unsafe { Gc::cast_unchecked::<VTableObject<T>>(self.inner) };
JsObject { inner }
}
#[must_use]
#[track_caller]
pub fn downcast_ref<T: NativeObject>(&self) -> Option<Ref<'_, T>> {
if self.is::<T>() {
let obj = self.borrow();
let obj = unsafe { GcRef::cast::<Object<T>>(obj) };
return Some(Ref::map(obj, |r| r.data()));
}
None
}
#[must_use]
#[track_caller]
pub fn downcast_mut<T: NativeObject>(&self) -> Option<RefMut<'_, T>> {
if self.is::<T>() {
let obj = self.borrow_mut();
let obj = unsafe { GcRefMut::cast::<Object<T>>(obj) };
return Some(RefMut::map(obj, |c| c.data_mut()));
}
None
}
#[inline]
#[must_use]
#[track_caller]
pub fn is<T: NativeObject>(&self) -> bool {
Gc::is::<VTableObject<T>>(&self.inner)
}
#[inline]
#[must_use]
#[track_caller]
pub fn is_ordinary(&self) -> bool {
self.is::<OrdinaryObject>()
}
#[inline]
#[must_use]
#[track_caller]
pub fn is_array(&self) -> bool {
std::ptr::eq(self.vtable(), &raw const ARRAY_EXOTIC_INTERNAL_METHODS)
}
pub(crate) fn deep_strict_equals_inner(
lhs: &Self,
rhs: &Self,
encounters: &mut HashSet<usize>,
context: &mut Context,
) -> JsResult<bool> {
fn key_loop(
lhs: &JsObject,
rhs: &JsObject,
encounters: &mut HashSet<usize>,
context: &mut Context,
) -> JsResult<bool> {
let l_keys = lhs.own_property_keys(context)?;
let r_keys = rhs.own_property_keys(context)?;
if l_keys.len() != r_keys.len() {
return Ok(false);
}
for key in &l_keys {
let vl = lhs.get_property(key);
let vr = rhs.get_property(key);
match (vl, vr) {
(None, None) => {}
(Some(vl), Some(vr)) => match (vl.value(), vr.value()) {
(None, None) => {}
(Some(lv), Some(rv)) => {
if !lv.deep_strict_equals_inner(rv, encounters, context)? {
return Ok(false);
}
}
_ => {
return Ok(false);
}
},
_ => {
return Ok(false);
}
}
}
Ok(true)
}
let addr_l = std::ptr::from_ref::<Self>(lhs) as usize;
let addr_r = std::ptr::from_ref::<Self>(rhs) as usize;
if addr_r == addr_l {
return Ok(true);
}
let contains_l = encounters.contains(&addr_l);
let contains_r = encounters.contains(&addr_r);
if contains_l || contains_r {
return Ok(false);
}
encounters.insert(addr_l);
encounters.insert(addr_r);
let result = key_loop(lhs, rhs, encounters, context);
encounters.remove(&addr_l);
encounters.remove(&addr_r);
result
}
#[inline]
pub fn deep_strict_equals(lhs: &Self, rhs: &Self, context: &mut Context) -> JsResult<bool> {
Self::deep_strict_equals_inner(lhs, rhs, &mut HashSet::new(), context)
}
pub fn to_primitive(
&self,
context: &mut Context,
preferred_type: PreferredType,
) -> JsResult<JsValue> {
let Some(exotic_to_prim) = self.get_method(JsSymbol::to_primitive(), context)? else {
let preferred_type = match preferred_type {
PreferredType::Default | PreferredType::Number => PreferredType::Number,
PreferredType::String => PreferredType::String,
};
return self.ordinary_to_primitive(context, preferred_type);
};
let hint = match preferred_type {
PreferredType::Default => js_string!("default"),
PreferredType::String => js_string!("string"),
PreferredType::Number => js_string!("number"),
}
.into();
let result = exotic_to_prim.call(&self.clone().into(), &[hint], context)?;
if result.is_object() {
Err(js_error!(
TypeError: "method `[Symbol.toPrimitive]` cannot return an object"
))
} else {
Ok(result)
}
}
pub(crate) fn ordinary_to_primitive(
&self,
context: &mut Context,
hint: PreferredType,
) -> JsResult<JsValue> {
debug_assert!(hint == PreferredType::String || hint == PreferredType::Number);
let recursion_limiter = RecursionLimiter::new(self.as_ref());
if recursion_limiter.live {
return Ok(match hint {
PreferredType::Number => JsValue::new(0),
PreferredType::String => JsValue::new(js_string!()),
PreferredType::Default => unreachable!("checked type hint in step 2"),
});
}
let method_names = if hint == PreferredType::String {
[js_string!("toString"), js_string!("valueOf")]
} else {
[js_string!("valueOf"), js_string!("toString")]
};
for name in method_names {
let method = self.get(name, context)?;
if let Some(method) = method.as_callable() {
let result = method.call(&self.clone().into(), &[], context)?;
if !result.is_object() {
return Ok(result);
}
}
}
Err(JsNativeError::typ()
.with_message("cannot convert object to primitive value")
.into())
}
pub fn to_property_descriptor(&self, context: &mut Context) -> JsResult<PropertyDescriptor> {
let mut desc = PropertyDescriptor::builder();
if let Some(enumerable) = self.try_get(js_string!("enumerable"), context)? {
desc = desc.enumerable(enumerable.to_boolean());
}
if let Some(configurable) = self.try_get(js_string!("configurable"), context)? {
desc = desc.configurable(configurable.to_boolean());
}
if let Some(value) = self.try_get(js_string!("value"), context)? {
desc = desc.value(value);
}
if let Some(writable) = self.try_get(js_string!("writable"), context)? {
desc = desc.writable(writable.to_boolean());
}
let get = if let Some(getter) = self.try_get(js_string!("get"), context)? {
if !getter.is_undefined() && getter.as_object().is_none_or(|o| !o.is_callable()) {
return Err(JsNativeError::typ()
.with_message("Property descriptor getter must be callable")
.into());
}
Some(getter)
} else {
None
};
let set = if let Some(setter) = self.try_get(js_string!("set"), context)? {
if !setter.is_undefined() && setter.as_object().is_none_or(|o| !o.is_callable()) {
return Err(JsNativeError::typ()
.with_message("Property descriptor setter must be callable")
.into());
}
Some(setter)
} else {
None
};
if get.as_ref().or(set.as_ref()).is_some() && desc.inner().is_data_descriptor() {
return Err(JsNativeError::typ()
.with_message(
"Invalid property descriptor.\
Cannot both specify accessors and a value or writable attribute",
)
.into());
}
desc = desc.maybe_get(get).maybe_set(set);
Ok(desc.build())
}
#[allow(clippy::assigning_clones)]
pub(crate) fn get_property(&self, key: &PropertyKey) -> Option<PropertyDescriptor> {
let mut obj = Some(self.clone());
while let Some(o) = obj {
if let Some(v) = o.borrow().properties.get(key) {
return Some(v);
}
obj = o.borrow().prototype().clone();
}
None
}
#[inline]
pub(crate) fn into_buffer_object(self) -> Result<BufferObject, JsObject> {
match self.downcast::<ArrayBuffer>() {
Ok(buffer) => Ok(BufferObject::Buffer(buffer)),
Err(object) => object
.downcast::<SharedArrayBuffer>()
.map(BufferObject::SharedBuffer),
}
}
}
impl<T: NativeObject> JsObject<T> {
#[must_use]
#[inline]
pub fn downgrade(&self) -> WeakJsObject<T> {
WeakJsObject(boa_gc::WeakGc::new(&self.inner))
}
#[inline]
#[must_use]
#[track_caller]
pub fn borrow(&self) -> Ref<'_, Object<T>> {
self.try_borrow().expect("Object already mutably borrowed")
}
#[inline]
#[must_use]
#[track_caller]
pub fn borrow_mut(&self) -> RefMut<'_, Object<T>> {
self.try_borrow_mut().expect("Object already borrowed")
}
#[inline]
pub fn try_borrow(&self) -> StdResult<Ref<'_, Object<T>>, BorrowError> {
self.inner.object.try_borrow().map_err(|_| BorrowError)
}
#[inline]
pub fn try_borrow_mut(&self) -> StdResult<RefMut<'_, Object<T>>, BorrowMutError> {
self.inner
.object
.try_borrow_mut()
.map_err(|_| BorrowMutError)
}
#[must_use]
#[inline]
pub fn equals(lhs: &Self, rhs: &Self) -> bool {
Gc::ptr_eq(lhs.inner(), rhs.inner())
}
#[inline]
#[must_use]
#[track_caller]
pub fn prototype(&self) -> JsPrototype {
self.borrow().prototype()
}
pub(crate) fn extensible(&self) -> bool {
self.borrow().extensible
}
#[inline]
#[track_caller]
#[allow(clippy::must_use_candidate)]
pub fn set_prototype(&self, prototype: JsPrototype) -> bool {
self.borrow_mut().set_prototype(prototype)
}
#[track_caller]
pub(crate) fn insert<K, P>(&self, key: K, property: P) -> bool
where
K: Into<PropertyKey>,
P: Into<PropertyDescriptor>,
{
self.borrow_mut().insert(key, property)
}
pub fn insert_property<K, P>(&self, key: K, property: P) -> bool
where
K: Into<PropertyKey>,
P: Into<PropertyDescriptor>,
{
self.insert(key.into(), property)
}
#[inline]
#[must_use]
pub fn is_callable(&self) -> bool {
!fn_addr_eq(
self.inner.vtable.__call__,
ORDINARY_INTERNAL_METHODS.__call__,
)
}
#[inline]
#[must_use]
pub fn is_constructor(&self) -> bool {
!fn_addr_eq(
self.inner.vtable.__construct__,
ORDINARY_INTERNAL_METHODS.__construct__,
)
}
pub(crate) fn vtable(&self) -> &'static InternalObjectMethods {
self.inner.vtable
}
pub(crate) fn inner(&self) -> &Gc<VTableObject<T>> {
&self.inner
}
pub(crate) fn from_inner(inner: Gc<VTableObject<T>>) -> Self {
Self { inner }
}
pub(crate) fn private_name(&self, description: JsString) -> PrivateName {
let ptr: *const _ = self.as_ref();
PrivateName::new(description, ptr.cast::<()>() as usize)
}
}
impl<T: NativeObject> JsObject<T> {
pub fn new<O: Into<Option<JsObject>>>(root_shape: &RootShape, prototype: O, data: T) -> Self {
let internal_methods = data.internal_methods();
let inner = Gc::new(VTableObject {
object: GcRefCell::new(Object {
data: ObjectData::new(data),
properties: PropertyMap::from_prototype_with_shared_shape(
root_shape,
prototype.into(),
),
extensible: true,
private_elements: ThinVec::new(),
}),
vtable: internal_methods,
});
Self { inner }
}
pub fn new_unique<O: Into<Option<JsObject>>>(prototype: O, data: T) -> Self {
let internal_methods = data.internal_methods();
let inner = Gc::new(VTableObject {
object: GcRefCell::new(Object {
data: ObjectData::new(data),
properties: PropertyMap::from_prototype_unique_shape(prototype.into()),
extensible: true,
private_elements: ThinVec::new(),
}),
vtable: internal_methods,
});
Self { inner }
}
#[must_use]
pub fn upcast(self) -> JsObject {
let inner = unsafe { Gc::cast_unchecked::<ErasedVTableObject>(self.inner) };
JsObject { inner }
}
}
impl<T: NativeObject> AsRef<GcRefCell<Object<T>>> for JsObject<T> {
#[inline]
fn as_ref(&self) -> &GcRefCell<Object<T>> {
&self.inner.object
}
}
impl<T: NativeObject> From<Gc<VTableObject<T>>> for JsObject<T> {
#[inline]
fn from(inner: Gc<VTableObject<T>>) -> Self {
Self { inner }
}
}
impl<T: NativeObject> PartialEq for JsObject<T> {
fn eq(&self, other: &Self) -> bool {
Self::equals(self, other)
}
}
impl<T: NativeObject> Eq for JsObject<T> {}
impl<T: NativeObject> Hash for JsObject<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(self.as_ref(), state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BorrowError;
impl Display for BorrowError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt("Object already mutably borrowed", f)
}
}
impl Error for BorrowError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BorrowMutError;
impl Display for BorrowMutError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt("Object already borrowed", f)
}
}
impl Error for BorrowMutError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum RecursionValueState {
Live,
Visited,
}
#[derive(Debug)]
pub struct RecursionLimiter {
top_level: bool,
ptr: usize,
pub visited: bool,
pub live: bool,
}
impl Drop for RecursionLimiter {
fn drop(&mut self) {
if self.top_level {
SEEN.with(|hm| hm.borrow_mut().clear());
} else if !self.live {
SEEN.with(|hm| {
hm.borrow_mut()
.insert(self.ptr, RecursionValueState::Visited)
});
}
}
}
thread_local! {
static SEEN: RefCell<HashMap<usize, RecursionValueState>> = RefCell::new(HashMap::new());
}
impl RecursionLimiter {
pub fn new<T: ?Sized>(o: &T) -> Self {
let ptr: *const _ = o;
let ptr = ptr.cast::<()>() as usize;
let (top_level, visited, live) = SEEN.with(|hm| {
let mut hm = hm.borrow_mut();
let top_level = hm.is_empty();
let old_state = hm.insert(ptr, RecursionValueState::Live);
(
top_level,
old_state == Some(RecursionValueState::Visited),
old_state == Some(RecursionValueState::Live),
)
});
Self {
top_level,
ptr,
visited,
live,
}
}
}
impl<T: NativeObject> Debug for JsObject<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let limiter = RecursionLimiter::new(self.as_ref());
if !limiter.visited && !limiter.live {
let ptr: *const _ = self.as_ref();
let ptr = ptr.cast::<()>();
let obj = self.borrow();
let kind = obj.data().type_name_of_value();
if self.is_callable() {
let name_prop = obj
.properties()
.get(&PropertyKey::String(js_string!("name")));
let name = match name_prop {
None => JsString::default(),
Some(prop) => prop
.value()
.and_then(JsValue::as_string)
.unwrap_or_default(),
};
return f.write_fmt(format_args!("({:?}) {:?} 0x{:X}", kind, name, ptr as usize));
}
f.write_fmt(format_args!("({:?}) 0x{:X}", kind, ptr as usize))
} else {
f.write_str("{ ... }")
}
}
}