Skip to content

Commit

Permalink
Introduce a Class map (#3315)
Browse files Browse the repository at this point in the history
* Improve `Class` and `ClassBuilder` ergonomics

* Fix tests

* Apply review

* Fix docs
  • Loading branch information
jedel1043 authored Sep 30, 2023
1 parent ebb40ae commit b03aa36
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 175 deletions.
6 changes: 1 addition & 5 deletions boa_engine/src/builtins/intl/segmenter/segments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ impl IntrinsicObject for Segments {

BuiltInBuilder::with_intrinsic::<Self>(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();
}

Expand Down
15 changes: 2 additions & 13 deletions boa_engine/src/builtins/iterable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,7 @@ impl IntrinsicObject for Iterator {
let _timer = Profiler::global().start_event("Iterator Prototype", "init");

BuiltInBuilder::with_intrinsic::<Self>(realm)
.static_method(
|v, _, _| Ok(v.clone()),
(JsSymbol::iterator(), js_string!("[Symbol.iterator]")),
0,
)
.static_method(|v, _, _| Ok(v.clone()), JsSymbol::iterator(), 0)
.build();
}

Expand All @@ -187,14 +183,7 @@ impl IntrinsicObject for AsyncIterator {
let _timer = Profiler::global().start_event("AsyncIteratorPrototype", "init");

BuiltInBuilder::with_intrinsic::<Self>(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();
}

Expand Down
30 changes: 5 additions & 25 deletions boa_engine/src/builtins/regexp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 1 addition & 7 deletions boa_engine/src/builtins/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ impl IntrinsicObject for String {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");

let symbol_iterator = JsSymbol::iterator();

let trim_start = BuiltInBuilder::callable(realm, Self::trim_start)
.length(0)
.name(js_string!("trimStart"))
Expand Down Expand Up @@ -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);

Expand Down
130 changes: 58 additions & 72 deletions boa_engine/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Self> {
//! // 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<Self> {
//! // This is equivalent to `String(arg)`.
//! let kind = args.get_or_undefined(0).to_string(context)?;
//!
Expand All @@ -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() {
Expand All @@ -66,99 +67,84 @@
//! [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<Self>;
/// 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<Self>;

/// 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<JsValue>
where
Self: Sized;
}

impl<T: Class> 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<JsValue>
where
Self: Sized,
{
if this.is_undefined() {
) -> JsResult<JsObject> {
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::<Self>().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)
.map(|val| val.as_object().cloned())
})
.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)
}
}

Expand All @@ -171,28 +157,29 @@ pub struct ClassBuilder<'ctx, 'host> {
impl<'ctx, 'host> ClassBuilder<'ctx, 'host> {
pub(crate) fn new<T>(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.
///
/// It is added to `prototype`.
pub fn method<N>(&mut self, name: N, length: usize, function: NativeFunction) -> &mut Self
where
N: AsRef<str>,
N: Into<FunctionBinding>,
{
self.builder
.method(function, js_string!(name.as_ref()), length);
self.builder.method(function, name, length);
self
}

Expand All @@ -206,10 +193,9 @@ impl<'ctx, 'host> ClassBuilder<'ctx, 'host> {
function: NativeFunction,
) -> &mut Self
where
N: AsRef<str>,
N: Into<FunctionBinding>,
{
self.builder
.static_method(function, js_string!(name.as_ref()), length);
self.builder.static_method(function, name, length);
self
}

Expand Down
14 changes: 11 additions & 3 deletions boa_engine/src/context/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit b03aa36

Please sign in to comment.