From b03aa360f80276303bbe225ca43e0e499820d47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Sat, 30 Sep 2023 15:08:40 +0000 Subject: [PATCH] Introduce a `Class` map (#3315) * Improve `Class` and `ClassBuilder` ergonomics * Fix tests * Apply review * Fix docs --- .../src/builtins/intl/segmenter/segments.rs | 6 +- boa_engine/src/builtins/iterable/mod.rs | 15 +- boa_engine/src/builtins/regexp/mod.rs | 30 +--- boa_engine/src/builtins/string/mod.rs | 8 +- boa_engine/src/class.rs | 130 ++++++++---------- boa_engine/src/context/intrinsics.rs | 14 +- boa_engine/src/context/mod.rs | 78 +++++++++-- boa_engine/src/host_defined.rs | 26 +--- boa_engine/src/object/mod.rs | 19 ++- boa_engine/src/realm.rs | 46 ++++++- boa_engine/src/string/common.rs | 13 ++ boa_engine/src/symbol.rs | 38 ++++- boa_examples/src/bin/classes.rs | 19 ++- boa_gc/src/cell.rs | 54 ++++++++ 14 files changed, 321 insertions(+), 175 deletions(-) diff --git a/boa_engine/src/builtins/intl/segmenter/segments.rs b/boa_engine/src/builtins/intl/segmenter/segments.rs index 143d798503..e7fb671f77 100644 --- a/boa_engine/src/builtins/intl/segmenter/segments.rs +++ b/boa_engine/src/builtins/intl/segmenter/segments.rs @@ -25,11 +25,7 @@ impl IntrinsicObject for Segments { BuiltInBuilder::with_intrinsic::(realm) .static_method(Self::containing, js_string!("containing"), 1) - .static_method( - Self::iterator, - (JsSymbol::iterator(), js_string!("[Symbol.iterator]")), - 0, - ) + .static_method(Self::iterator, JsSymbol::iterator(), 0) .build(); } diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index 2db05486df..0074ea2db1 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -161,11 +161,7 @@ impl IntrinsicObject for Iterator { let _timer = Profiler::global().start_event("Iterator Prototype", "init"); BuiltInBuilder::with_intrinsic::(realm) - .static_method( - |v, _, _| Ok(v.clone()), - (JsSymbol::iterator(), js_string!("[Symbol.iterator]")), - 0, - ) + .static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0) .build(); } @@ -187,14 +183,7 @@ impl IntrinsicObject for AsyncIterator { let _timer = Profiler::global().start_event("AsyncIteratorPrototype", "init"); BuiltInBuilder::with_intrinsic::(realm) - .static_method( - |v, _, _| Ok(v.clone()), - ( - JsSymbol::async_iterator(), - js_string!("[Symbol.asyncIterator]"), - ), - 0, - ) + .static_method(|v, _, _| Ok(v.clone()), JsSymbol::async_iterator(), 0) .build(); } diff --git a/boa_engine/src/builtins/regexp/mod.rs b/boa_engine/src/builtins/regexp/mod.rs index c5eff592ea..9ca6a727e3 100644 --- a/boa_engine/src/builtins/regexp/mod.rs +++ b/boa_engine/src/builtins/regexp/mod.rs @@ -95,31 +95,11 @@ impl IntrinsicObject for RegExp { .method(Self::test, js_string!("test"), 1) .method(Self::exec, js_string!("exec"), 1) .method(Self::to_string, js_string!("toString"), 0) - .method( - Self::r#match, - (JsSymbol::r#match(), js_string!("[Symbol.match]")), - 1, - ) - .method( - Self::match_all, - (JsSymbol::match_all(), js_string!("[Symbol.matchAll]")), - 1, - ) - .method( - Self::replace, - (JsSymbol::replace(), js_string!("[Symbol.replace]")), - 2, - ) - .method( - Self::search, - (JsSymbol::search(), js_string!("[Symbol.search]")), - 1, - ) - .method( - Self::split, - (JsSymbol::split(), js_string!("[Symbol.split]")), - 2, - ) + .method(Self::r#match, JsSymbol::r#match(), 1) + .method(Self::match_all, JsSymbol::match_all(), 1) + .method(Self::replace, JsSymbol::replace(), 2) + .method(Self::search, JsSymbol::search(), 1) + .method(Self::split, JsSymbol::split(), 2) .accessor( js_string!("hasIndices"), Some(get_has_indices), diff --git a/boa_engine/src/builtins/string/mod.rs b/boa_engine/src/builtins/string/mod.rs index fb3c76e20e..8147b2c5a0 100644 --- a/boa_engine/src/builtins/string/mod.rs +++ b/boa_engine/src/builtins/string/mod.rs @@ -78,8 +78,6 @@ impl IntrinsicObject for String { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); - let symbol_iterator = JsSymbol::iterator(); - let trim_start = BuiltInBuilder::callable(realm, Self::trim_start) .length(0) .name(js_string!("trimStart")) @@ -150,11 +148,7 @@ impl IntrinsicObject for String { .method(Self::match_all, js_string!("matchAll"), 1) .method(Self::replace, js_string!("replace"), 2) .method(Self::replace_all, js_string!("replaceAll"), 2) - .method( - Self::iterator, - (symbol_iterator, js_string!("[Symbol.iterator]")), - 0, - ) + .method(Self::iterator, JsSymbol::iterator(), 0) .method(Self::search, js_string!("search"), 1) .method(Self::at, js_string!("at"), 1); diff --git a/boa_engine/src/class.rs b/boa_engine/src/class.rs index 5c1fd385c6..e44e8e934e 100644 --- a/boa_engine/src/class.rs +++ b/boa_engine/src/class.rs @@ -8,10 +8,11 @@ //! # class::{Class, ClassBuilder}, //! # Context, JsResult, JsValue, //! # JsArgs, +//! # js_string, //! # }; //! # use boa_gc::{Finalize, Trace}; //! # -//! // This does not have to be an enum it can also be a struct. +//! // Can also be a struct containing `Trace` types. //! #[derive(Debug, Trace, Finalize)] //! enum Animal { //! Cat, @@ -26,8 +27,8 @@ //! // We set the length to `1` since we accept 1 arguments in the constructor. //! const LENGTH: usize = 1; //! -//! // This is what is called when we do `new Animal()` -//! fn constructor(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { +//! // This is what is called when we do `new Animal()` to construct the inner data of the class. +//! fn make_data(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { //! // This is equivalent to `String(arg)`. //! let kind = args.get_or_undefined(0).to_string(context)?; //! @@ -43,7 +44,7 @@ //! /// This is where the object is initialized. //! fn init(class: &mut ClassBuilder) -> JsResult<()> { //! class.method( -//! "speak", +//! js_string!("speak"), //! 0, //! NativeFunction::from_fn_ptr(|this, _args, _ctx| { //! if let Some(object) = this.as_object() { @@ -66,83 +67,68 @@ //! [class-trait]: ./trait.Class.html use crate::{ + context::intrinsics::StandardConstructor, error::JsNativeError, - js_string, native_function::NativeFunction, - object::{ConstructorBuilder, JsFunction, JsObject, NativeObject, ObjectData, PROTOTYPE}, + object::{ + ConstructorBuilder, FunctionBinding, JsFunction, JsObject, NativeObject, ObjectData, + PROTOTYPE, + }, property::{Attribute, PropertyDescriptor, PropertyKey}, Context, JsResult, JsValue, }; /// Native class. pub trait Class: NativeObject + Sized { - /// The binding name of the object. + /// The binding name of this class. const NAME: &'static str; - /// The amount of arguments the class `constructor` takes, default is `0`. + /// The amount of arguments this class' constructor takes. Default is `0`. const LENGTH: usize = 0; - /// The attributes the class will be binded with, default is `writable`, `enumerable`, `configurable`. + /// The property attributes of this class' constructor in the global object. + /// Default is `writable`, `enumerable`, `configurable`. const ATTRIBUTES: Attribute = Attribute::all(); - /// The constructor of the class. - fn constructor(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult; + /// Creates the internal data for an instance of this class. + /// + /// This method can also be called the "native constructor" of this class. + fn make_data(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult; - /// Initializes the internals and the methods of the class. + /// Initializes the properties and methods of this class. fn init(class: &mut ClassBuilder<'_, '_>) -> JsResult<()>; -} - -/// This is a wrapper around `Class::constructor` that sets the internal data of a class. -/// -/// This is automatically implemented, when a type implements `Class`. -pub trait ClassConstructor: Class { - /// The raw constructor that matches the `NativeFunction` signature. - fn raw_constructor( - this: &JsValue, - args: &[JsValue], - context: &mut Context<'_>, - ) -> JsResult - where - Self: Sized; -} -impl ClassConstructor for T { - fn raw_constructor( - this: &JsValue, + /// Creates a new [`JsObject`] with its internal data set to the result of calling `Self::make_data`. + /// + /// # Note + /// + /// This will throw an error if this class is not registered in the context's active realm. + /// See [`Context::register_global_class`]. + /// + /// # Warning + /// + /// Overriding this method could be useful for certain usages, but incorrectly implementing this + /// could lead to weird errors like missing inherited methods or incorrect internal data. + fn construct( + new_target: &JsValue, args: &[JsValue], context: &mut Context<'_>, - ) -> JsResult - where - Self: Sized, - { - if this.is_undefined() { + ) -> JsResult { + if new_target.is_undefined() { return Err(JsNativeError::typ() .with_message(format!( "cannot call constructor of native class `{}` without new", - T::NAME + Self::NAME )) .into()); } - let class = context.global_object().get(js_string!(T::NAME), context)?; - let JsValue::Object(ref class_constructor) = class else { - return Err(JsNativeError::typ() - .with_message(format!( - "invalid constructor for native class `{}` ", - T::NAME - )) - .into()); - }; + let class = context.get_global_class::().ok_or_else(|| { + JsNativeError::typ().with_message(format!( + "could not find native class `{}` in the map of registered classes", + Self::NAME + )) + })?; - let JsValue::Object(ref class_prototype) = class_constructor.get(PROTOTYPE, context)? - else { - return Err(JsNativeError::typ() - .with_message(format!( - "invalid default prototype for native class `{}`", - T::NAME - )) - .into()); - }; - - let prototype = this + let prototype = new_target .as_object() .map(|obj| { obj.get(PROTOTYPE, context) @@ -150,15 +136,15 @@ impl ClassConstructor for T { }) .transpose()? .flatten() - .unwrap_or_else(|| class_prototype.clone()); + .unwrap_or_else(|| class.prototype()); - let native_instance = Self::constructor(this, args, context)?; - let object_instance = JsObject::from_proto_and_data_with_shared_shape( + let data = Self::make_data(new_target, args, context)?; + let instance = JsObject::from_proto_and_data_with_shared_shape( context.root_shape(), prototype, - ObjectData::native_object(native_instance), + ObjectData::native_object(data), ); - Ok(object_instance.into()) + Ok(instance) } } @@ -171,17 +157,19 @@ pub struct ClassBuilder<'ctx, 'host> { impl<'ctx, 'host> ClassBuilder<'ctx, 'host> { pub(crate) fn new(context: &'ctx mut Context<'host>) -> Self where - T: ClassConstructor, + T: Class, { - let mut builder = - ConstructorBuilder::new(context, NativeFunction::from_fn_ptr(T::raw_constructor)); + let mut builder = ConstructorBuilder::new( + context, + NativeFunction::from_fn_ptr(|t, a, c| T::construct(t, a, c).map(JsValue::from)), + ); builder.name(T::NAME); builder.length(T::LENGTH); Self { builder } } - pub(crate) fn build(self) -> JsFunction { - JsFunction::from_object_unchecked(self.builder.build().into()) + pub(crate) fn build(self) -> StandardConstructor { + self.builder.build() } /// Add a method to the class. @@ -189,10 +177,9 @@ impl<'ctx, 'host> ClassBuilder<'ctx, 'host> { /// It is added to `prototype`. pub fn method(&mut self, name: N, length: usize, function: NativeFunction) -> &mut Self where - N: AsRef, + N: Into, { - self.builder - .method(function, js_string!(name.as_ref()), length); + self.builder.method(function, name, length); self } @@ -206,10 +193,9 @@ impl<'ctx, 'host> ClassBuilder<'ctx, 'host> { function: NativeFunction, ) -> &mut Self where - N: AsRef, + N: Into, { - self.builder - .static_method(function, js_string!(name.as_ref()), length); + self.builder.static_method(function, name, length); self } diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 2547dea121..70d58198e9 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -58,8 +58,8 @@ impl Intrinsics { } } -/// Store a builtin constructor (such as `Object`) and its corresponding prototype. -#[derive(Debug, Trace, Finalize)] +/// Stores a constructor (such as `Object`) and its corresponding prototype. +#[derive(Debug, Trace, Finalize, Clone)] pub struct StandardConstructor { constructor: JsFunction, prototype: JsObject, @@ -75,6 +75,14 @@ impl Default for StandardConstructor { } impl StandardConstructor { + /// Creates a new `StandardConstructor` from the constructor and the prototype. + pub(crate) fn new(constructor: JsFunction, prototype: JsObject) -> Self { + Self { + constructor, + prototype, + } + } + /// Build a constructor with a defined prototype. fn with_prototype(prototype: JsObject) -> Self { Self { @@ -85,7 +93,7 @@ impl StandardConstructor { /// Return the prototype of the constructor object. /// - /// This is the same as `Object.prototype`, `Array.prototype`, etc + /// This is the same as `Object.prototype`, `Array.prototype`, etc. #[inline] #[must_use] pub fn prototype(&self) -> JsObject { diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 7795fcec9d..32dd3008f7 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -29,7 +29,7 @@ use crate::{ realm::Realm, script::Script, vm::{ActiveRunnable, CallFrame, Vm}, - JsResult, JsString, JsValue, Source, + JsNativeError, JsResult, JsString, JsValue, Source, }; use boa_ast::{expression::Identifier, StatementList}; use boa_interner::Interner; @@ -37,6 +37,8 @@ use boa_profiler::Profiler; use crate::vm::RuntimeLimits; +use self::intrinsics::StandardConstructor; + /// ECMAScript context. It is the primary way to interact with the runtime. /// /// `Context`s constructed in a thread share the same runtime, therefore it @@ -317,9 +319,9 @@ impl<'host> Context<'host> { Ok(()) } - /// Register a global class of type `T`, where `T` implements `Class`. + /// Registers a global class `C` in the currently active realm. /// - /// It will return an error if the global property is already defined. + /// Errors if the class has already been registered. /// /// # Example /// ```ignore @@ -330,28 +332,74 @@ impl<'host> Context<'host> { /// // ... /// } /// - /// context.register_global_class::(); + /// context.register_global_class::()?; /// ``` - pub fn register_global_class(&mut self) -> JsResult<()> - where - T: Class, - { - let mut class_builder = ClassBuilder::new::(self); - T::init(&mut class_builder)?; + pub fn register_global_class(&mut self) -> JsResult<()> { + if self.realm.has_class::() { + return Err(JsNativeError::typ() + .with_message("cannot register a class twice") + .into()); + } + + let mut class_builder = ClassBuilder::new::(self); + C::init(&mut class_builder)?; let class = class_builder.build(); let property = PropertyDescriptor::builder() - .value(class) - .writable(T::ATTRIBUTES.writable()) - .enumerable(T::ATTRIBUTES.enumerable()) - .configurable(T::ATTRIBUTES.configurable()); + .value(class.constructor()) + .writable(C::ATTRIBUTES.writable()) + .enumerable(C::ATTRIBUTES.enumerable()) + .configurable(C::ATTRIBUTES.configurable()); self.global_object() - .define_property_or_throw(js_string!(T::NAME), property, self)?; + .define_property_or_throw(js_string!(C::NAME), property, self)?; + self.realm.register_class::(class); Ok(()) } + /// Removes the global class `C` from the currently active realm, returning the constructor + /// and prototype of the class if `C` was registered. + /// + /// # Note + /// + /// This makes the constructor return an error on further calls, but note that this won't protect + /// static properties from being accessed within variables that stored the constructor before being + /// unregistered. If you need that functionality, you can use a static accessor that first checks + /// if the class is registered ([`Context::has_global_class`]) before returning the static value. + /// + /// # Example + /// ```ignore + /// #[derive(Debug, Trace, Finalize)] + /// struct MyClass; + /// + /// impl Class for MyClass { + /// // ... + /// } + /// + /// context.register_global_class::()?; + /// // ... code + /// context.unregister_global_class::()?; + /// ``` + pub fn unregister_global_class(&mut self) -> JsResult> { + self.global_object() + .delete_property_or_throw(js_string!(C::NAME), self)?; + Ok(self.realm.unregister_class::()) + } + + /// Checks if the currently active realm has the global class `C` registered. + #[must_use] + pub fn has_global_class(&self) -> bool { + self.realm.has_class::() + } + + /// Gets the constructor and prototype of the global class `C` if the currently active realm has + /// that class registered. + #[must_use] + pub fn get_global_class(&self) -> Option { + self.realm.get_class::() + } + /// Gets the string interner. #[inline] #[must_use] diff --git a/boa_engine/src/host_defined.rs b/boa_engine/src/host_defined.rs index f56e830dc6..ff66267fbf 100644 --- a/boa_engine/src/host_defined.rs +++ b/boa_engine/src/host_defined.rs @@ -73,20 +73,12 @@ impl HostDefined { /// Panics if [`HostDefined`] field is borrowed. #[track_caller] pub fn get(&self) -> Option> { - let state = self.state.borrow(); - - state - .get(&TypeId::of::()) - .map(Box::as_ref) - .and_then(::downcast_ref::)?; - - Some(GcRef::map(state, |state| { + GcRef::try_map(self.state.borrow(), |state| { state .get(&TypeId::of::()) .map(Box::as_ref) .and_then(::downcast_ref::) - .expect("Should not fail") - })) + }) } /// Get type T from [`HostDefined`], if it exits. @@ -96,23 +88,15 @@ impl HostDefined { /// Panics if [`HostDefined`] field is borrowed. #[track_caller] pub fn get_mut(&self) -> Option> { - let mut state = self.state.borrow_mut(); - - state - .get_mut(&TypeId::of::()) - .map(Box::as_mut) - .and_then(::downcast_mut::)?; - - Some(GcRefMut::map( - state, + GcRefMut::try_map( + self.state.borrow_mut(), |state: &mut FxHashMap>| { state .get_mut(&TypeId::of::()) .map(Box::as_mut) .and_then(::downcast_mut::) - .expect("Should not fail") }, - )) + ) } /// Clears all the objects. diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 11176a2bf1..293f705674 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -56,6 +56,7 @@ use crate::{ typed_array::{integer_indexed_object::IntegerIndexed, TypedArrayKind}, DataView, Date, Promise, RegExp, }, + context::intrinsics::StandardConstructor, js_string, module::ModuleNamespace, native_function::NativeFunction, @@ -2004,8 +2005,8 @@ impl Object { /// /// There are two implementations: /// - From a single type `T` which implements `Into` which sets the binding -/// name and the function name to the same value -/// - From a tuple `(B: Into, N: AsRef)` the `B` is the binding name +/// name and the function name to the same value. +/// - From a tuple `(B: Into, N: Into)`, where the `B` is the binding name /// and the `N` is the function name. #[derive(Debug, Clone)] pub struct FunctionBinding { @@ -2023,6 +2024,16 @@ impl From for FunctionBinding { } } +impl From for FunctionBinding { + #[inline] + fn from(binding: JsSymbol) -> Self { + Self { + name: binding.fn_name(), + binding: binding.into(), + } + } +} + impl From<(B, N)> for FunctionBinding where B: Into, @@ -2510,7 +2521,7 @@ impl<'ctx, 'host> ConstructorBuilder<'ctx, 'host> { /// Build the constructor function object. #[must_use] - pub fn build(mut self) -> JsFunction { + pub fn build(mut self) -> StandardConstructor { // Create the native function let function = Function::new( FunctionKind::Native { @@ -2593,6 +2604,6 @@ impl<'ctx, 'host> ConstructorBuilder<'ctx, 'host> { ); } - JsFunction::from_object_unchecked(constructor) + StandardConstructor::new(JsFunction::from_object_unchecked(constructor), prototype) } } diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index 1dec3e0e1c..f0ad3c88da 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -6,8 +6,16 @@ //! //! A realm is represented in this implementation as a Realm struct with the fields specified from the spec. +use std::any::TypeId; + +use rustc_hash::FxHashMap; + use crate::{ - context::{intrinsics::Intrinsics, HostHooks}, + class::Class, + context::{ + intrinsics::{Intrinsics, StandardConstructor}, + HostHooks, + }, environments::DeclarativeEnvironment, module::Module, object::shape::RootShape, @@ -15,7 +23,6 @@ use crate::{ }; use boa_gc::{Finalize, Gc, GcRefCell, Trace}; use boa_profiler::Profiler; -use rustc_hash::FxHashMap; /// Representation of a Realm. /// @@ -52,6 +59,7 @@ struct Inner { global_this: JsObject, template_map: GcRefCell>, loaded_modules: GcRefCell>, + host_classes: GcRefCell>, host_defined: HostDefined, } @@ -77,6 +85,7 @@ impl Realm { global_this, template_map: GcRefCell::default(), loaded_modules: GcRefCell::default(), + host_classes: GcRefCell::default(), host_defined: HostDefined::default(), }), }; @@ -102,6 +111,25 @@ impl Realm { &self.inner.host_defined } + /// Checks if this `Realm` has the class `C` registered into its class map. + #[must_use] + pub fn has_class(&self) -> bool { + self.inner + .host_classes + .borrow() + .contains_key(&TypeId::of::()) + } + + /// Gets the constructor and prototype of the class `C` if it is registered in the class map. + #[must_use] + pub fn get_class(&self) -> Option { + self.inner + .host_classes + .borrow() + .get(&TypeId::of::()) + .cloned() + } + pub(crate) fn environment(&self) -> &Gc { &self.inner.environment } @@ -142,6 +170,20 @@ impl Realm { self.inner.template_map.borrow().get(&site).cloned() } + pub(crate) fn register_class(&self, spec: StandardConstructor) { + self.inner + .host_classes + .borrow_mut() + .insert(TypeId::of::(), spec); + } + + pub(crate) fn unregister_class(&self) -> Option { + self.inner + .host_classes + .borrow_mut() + .remove(&TypeId::of::()) + } + pub(crate) fn addr(&self) -> *const () { let ptr: *const _ = &*self.inner; ptr.cast() diff --git a/boa_engine/src/string/common.rs b/boa_engine/src/string/common.rs index 8e8b05febe..88b2166508 100644 --- a/boa_engine/src/string/common.rs +++ b/boa_engine/src/string/common.rs @@ -94,6 +94,19 @@ impl StaticJsStrings { (SYMBOL_TO_PRIMITIVE, "Symbol.toPrimitive"), (SYMBOL_TO_STRING_TAG, "Symbol.toStringTag"), (SYMBOL_UNSCOPABLES, "Symbol.unscopables"), + (FN_SYMBOL_ASYNC_ITERATOR, "[Symbol.asyncIterator]"), + (FN_SYMBOL_HAS_INSTANCE, "[Symbol.hasInstance]"), + (FN_SYMBOL_IS_CONCAT_SPREADABLE, "[Symbol.isConcatSpreadable]"), + (FN_SYMBOL_ITERATOR, "[Symbol.iterator]"), + (FN_SYMBOL_MATCH, "[Symbol.match]"), + (FN_SYMBOL_MATCH_ALL, "[Symbol.matchAll]"), + (FN_SYMBOL_REPLACE, "[Symbol.replace]"), + (FN_SYMBOL_SEARCH, "[Symbol.search]"), + (FN_SYMBOL_SPECIES, "[Symbol.species]"), + (FN_SYMBOL_SPLIT, "[Symbol.split]"), + (FN_SYMBOL_TO_PRIMITIVE, "[Symbol.toPrimitive]"), + (FN_SYMBOL_TO_STRING_TAG, "[Symbol.toStringTag]"), + (FN_SYMBOL_UNSCOPABLES, "[Symbol.unscopables]"), // Builtins (ARRAY, "Array"), (ARRAY_BUFFER, "ArrayBuffer"), diff --git a/boa_engine/src/symbol.rs b/boa_engine/src/symbol.rs index a0733ff4f9..375eada8bf 100644 --- a/boa_engine/src/symbol.rs +++ b/boa_engine/src/symbol.rs @@ -96,6 +96,24 @@ impl WellKnown { } } + const fn fn_name(self) -> JsString { + match self { + Self::AsyncIterator => StaticJsStrings::FN_SYMBOL_ASYNC_ITERATOR, + Self::HasInstance => StaticJsStrings::FN_SYMBOL_HAS_INSTANCE, + Self::IsConcatSpreadable => StaticJsStrings::FN_SYMBOL_IS_CONCAT_SPREADABLE, + Self::Iterator => StaticJsStrings::FN_SYMBOL_ITERATOR, + Self::Match => StaticJsStrings::FN_SYMBOL_MATCH, + Self::MatchAll => StaticJsStrings::FN_SYMBOL_MATCH_ALL, + Self::Replace => StaticJsStrings::FN_SYMBOL_REPLACE, + Self::Search => StaticJsStrings::FN_SYMBOL_SEARCH, + Self::Species => StaticJsStrings::FN_SYMBOL_SPECIES, + Self::Split => StaticJsStrings::FN_SYMBOL_SPLIT, + Self::ToPrimitive => StaticJsStrings::FN_SYMBOL_TO_PRIMITIVE, + Self::ToStringTag => StaticJsStrings::FN_SYMBOL_TO_STRING_TAG, + Self::Unscopables => StaticJsStrings::FN_SYMBOL_UNSCOPABLES, + } + } + const fn hash(self) -> u64 { self as u64 } @@ -163,7 +181,7 @@ impl JsSymbol { }) } - /// Returns the `Symbol`s description. + /// Returns the `Symbol` description. #[inline] #[must_use] pub fn description(&self) -> Option { @@ -182,6 +200,24 @@ impl JsSymbol { } } + /// Returns the `Symbol` as a function name. + /// + /// Equivalent to `[description]`, but returns the empty string if the symbol doesn't have a + /// description. + #[inline] + #[must_use] + pub fn fn_name(&self) -> JsString { + if let UnwrappedTagged::Tag(tag) = self.repr.unwrap() { + // SAFETY: All tagged reprs always come from `WellKnown` itself, making + // this operation always safe. + let wk = unsafe { WellKnown::from_tag(tag).unwrap_unchecked() }; + return wk.fn_name(); + } + self.description() + .map(|s| js_string!(utf16!("["), &*s, utf16!("]"))) + .unwrap_or_default() + } + /// Returns the `Symbol`s hash. /// /// The hash is guaranteed to be unique. diff --git a/boa_examples/src/bin/classes.rs b/boa_examples/src/bin/classes.rs index 1f3f138d36..48144250da 100644 --- a/boa_examples/src/bin/classes.rs +++ b/boa_examples/src/bin/classes.rs @@ -64,18 +64,19 @@ impl Class for Person { // NOTE: The default value of `LENGTH` is `0`. const LENGTH: usize = 2; - // This is what is called when we construct a `Person` with the expression `new Person()`. - fn constructor(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + // This is what is internally called when we construct a `Person` with the expression `new Person()`. + fn make_data(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { // We get the first argument. If it is unavailable we default to `undefined`, // and then we call `to_string()`. // // This is equivalent to `String(arg)`. let name = args.get_or_undefined(0).to_string(context)?; + // We get the second argument. If it is unavailable we default to `undefined`, // and then we call `to_u32`. // // This is equivalent to `arg | 0`. - let age = args.get(1).cloned().unwrap_or_default().to_u32(context)?; + let age = args.get_or_undefined(1).to_u32(context)?; // We construct a new native struct `Person` let person = Person { name, age }; @@ -88,14 +89,18 @@ impl Class for Person { // We add a inheritable method `sayHello` with `0` arguments of length. // // This function is added to the `Person` prototype. - class.method("sayHello", 0, NativeFunction::from_fn_ptr(Self::say_hello)); + class.method( + js_string!("sayHello"), + 0, + NativeFunction::from_fn_ptr(Self::say_hello), + ); // We add a static method `is` using a closure, but it must be convertible - // to a NativeFunction. - // This means it must not contain state, or the code won't compile. + // to a `NativeFunction`. The `NativeFunction` API has more information on which type of + // Rust functions can be used to create `NativeFunction`s. // // This function is added to the `Person` class. class.static_method( - "is", + js_string!("is"), 1, NativeFunction::from_fn_ptr(|_this, args, _ctx| { if let Some(arg) = args.get(0) { diff --git a/boa_gc/src/cell.rs b/boa_gc/src/cell.rs index 3196f678bc..e1cd4978b3 100644 --- a/boa_gc/src/cell.rs +++ b/boa_gc/src/cell.rs @@ -269,6 +269,31 @@ impl<'a, T: ?Sized> GcRef<'a, T> { } } + /// Tries to make a new `GcCellRef` from a component of the borrowed data, returning `None` + /// if the mapping function returns `None`. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as `GcCellRef::try_map(...)`. + /// A method would interfere with methods of the same name on the contents + /// of a `GcCellRef` used through `Deref`. + pub fn try_map(orig: Self, f: F) -> Option> + where + U: ?Sized, + F: FnOnce(&T) -> Option<&U>, + { + let ret = GcRef { + flags: orig.flags, + value: f(orig.value)?, + }; + + // We have to tell the compiler not to call the destructor of GcCellRef, + // because it will update the borrow flags. + std::mem::forget(orig); + + Some(ret) + } + /// Makes a new `GcCellRef` from a component of the borrowed data. /// /// The `GcCell` is already immutably borrowed, so this cannot fail. @@ -362,6 +387,35 @@ pub struct GcRefMut<'a, T: ?Sized + 'static, U: ?Sized = T> { } impl<'a, T: ?Sized, U: ?Sized> GcRefMut<'a, T, U> { + /// Tries to make a new `GcCellRefMut` for a component of the borrowed data, returning `None` + /// if the mapping function returns `None`. + /// + /// The `GcCellRefMut` is already mutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as + /// `GcCellRefMut::map(...)`. A method would interfere with methods of the same + /// name on the contents of a `GcCell` used through `Deref`. + pub fn try_map(orig: Self, f: F) -> Option> + where + V: ?Sized, + F: FnOnce(&mut U) -> Option<&mut V>, + { + #[allow(trivial_casts)] + // SAFETY: This is safe as `GcCellRefMut` is already borrowed, so the value is rooted. + let value = unsafe { &mut *(orig.value as *mut U) }; + + let ret = GcRefMut { + gc_cell: orig.gc_cell, + value: f(value)?, + }; + + // We have to tell the compiler not to call the destructor of GcCellRef, + // because it will update the borrow flags. + std::mem::forget(orig); + + Some(ret) + } + /// Makes a new `GcCellRefMut` for a component of the borrowed data, e.g., an enum /// variant. ///