diff --git a/boa_cli/src/debug/mod.rs b/boa_cli/src/debug/mod.rs index 6701a38106..4e2f9409ea 100644 --- a/boa_cli/src/debug/mod.rs +++ b/boa_cli/src/debug/mod.rs @@ -8,10 +8,12 @@ mod gc; mod object; mod optimizer; mod realm; +mod shape; fn create_boa_object(context: &mut Context<'_>) -> JsObject { let function_module = function::create_object(context); let object_module = object::create_object(context); + let shape_module = shape::create_object(context); let optimizer_module = optimizer::create_object(context); let gc_module = gc::create_object(context); let realm_module = realm::create_object(context); @@ -27,6 +29,11 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject { object_module, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) + .property( + "shape", + shape_module, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) .property( "optimizer", optimizer_module, diff --git a/boa_cli/src/debug/shape.rs b/boa_cli/src/debug/shape.rs new file mode 100644 index 0000000000..167bc5b3c4 --- /dev/null +++ b/boa_cli/src/debug/shape.rs @@ -0,0 +1,66 @@ +use boa_engine::{ + js_string, object::ObjectInitializer, Context, JsArgs, JsNativeError, JsObject, JsResult, + JsValue, NativeFunction, +}; + +fn get_object(args: &[JsValue], position: usize) -> JsResult<&JsObject> { + let value = args.get_or_undefined(position); + + let Some(object) = value.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!("expected object in argument position {position}, got {}", value.type_of())) + .into()); + }; + + Ok(object) +} + +/// Returns object's shape pointer in memory. +fn id(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult { + let object = get_object(args, 0)?; + let object = object.borrow(); + let shape = object.shape(); + Ok(format!("0x{:X}", shape.to_addr_usize()).into()) +} + +/// Returns object's shape type. +fn r#type(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult { + let object = get_object(args, 0)?; + let object = object.borrow(); + let shape = object.shape(); + + Ok(if shape.is_shared() { + js_string!("shared") + } else { + js_string!("unique") + } + .into()) +} + +/// Returns object's shape type. +fn same(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult { + let lhs = get_object(args, 0)?; + let rhs = get_object(args, 1)?; + + let lhs_shape_ptr = { + let object = lhs.borrow(); + let shape = object.shape(); + shape.to_addr_usize() + }; + + let rhs_shape_ptr = { + let object = rhs.borrow(); + let shape = object.shape(); + shape.to_addr_usize() + }; + + Ok(JsValue::new(lhs_shape_ptr == rhs_shape_ptr)) +} + +pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { + ObjectInitializer::new(context) + .function(NativeFunction::from_fn_ptr(id), "id", 1) + .function(NativeFunction::from_fn_ptr(r#type), "type", 1) + .function(NativeFunction::from_fn_ptr(same), "same", 2) + .build() +} diff --git a/boa_engine/benches/full.rs b/boa_engine/benches/full.rs index a5591d63ab..5317bbdb65 100644 --- a/boa_engine/benches/full.rs +++ b/boa_engine/benches/full.rs @@ -1,7 +1,8 @@ //! Benchmarks of the whole execution engine in Boa. use boa_engine::{ - context::DefaultHooks, optimizer::OptimizerOptions, realm::Realm, Context, Source, + context::DefaultHooks, object::shape::SharedShape, optimizer::OptimizerOptions, realm::Realm, + Context, Source, }; use criterion::{criterion_group, criterion_main, Criterion}; use std::hint::black_box; @@ -15,7 +16,8 @@ static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; fn create_realm(c: &mut Criterion) { c.bench_function("Create Realm", move |b| { - b.iter(|| Realm::create(&DefaultHooks)) + let root_shape = SharedShape::root(); + b.iter(|| Realm::create(&DefaultHooks, &root_shape)) }); } diff --git a/boa_engine/src/builtins/array/array_iterator.rs b/boa_engine/src/builtins/array/array_iterator.rs index 4f8cf2d3f5..417efb815a 100644 --- a/boa_engine/src/builtins/array/array_iterator.rs +++ b/boa_engine/src/builtins/array/array_iterator.rs @@ -84,7 +84,8 @@ impl ArrayIterator { kind: PropertyNameKind, context: &Context<'_>, ) -> JsValue { - let array_iterator = JsObject::from_proto_and_data( + let array_iterator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().objects().iterator_prototypes().array(), ObjectData::array_iterator(Self::new(array, kind)), ); diff --git a/boa_engine/src/builtins/array/mod.rs b/boa_engine/src/builtins/array/mod.rs index ec398f5e06..f7374a0291 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -47,16 +47,15 @@ impl IntrinsicObject for Array { let symbol_iterator = JsSymbol::iterator(); let symbol_unscopables = JsSymbol::unscopables(); - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); - let values_function = BuiltInBuilder::with_object( + let values_function = BuiltInBuilder::callable_with_object( realm, realm.intrinsics().objects().array_prototype_values().into(), + Self::values, ) - .callable(Self::values) .name("values") .build(); @@ -217,17 +216,18 @@ impl BuiltInConstructor for Array { // b. Let array be ? ArrayCreate(numberOfArgs, proto). let array = Self::array_create(number_of_args as u64, Some(prototype), context)?; + // c. Let k be 0. // d. Repeat, while k < numberOfArgs, - for (i, item) in args.iter().cloned().enumerate() { - // i. Let Pk be ! ToString(𝔽(k)). - // ii. Let itemK be values[k]. - // iii. Perform ! CreateDataPropertyOrThrow(array, Pk, itemK). - array - .create_data_property_or_throw(i, item, context) - .expect("this CreateDataPropertyOrThrow must not fail"); - // iv. Set k to k + 1. - } + // i. Let Pk be ! ToString(𝔽(k)). + // ii. Let itemK be values[k]. + // iii. Perform ! CreateDataPropertyOrThrow(array, Pk, itemK). + // iv. Set k to k + 1. + array + .borrow_mut() + .properties_mut() + .override_indexed_properties(args.iter().cloned().collect()); + // e. Assert: The mathematical value of array's "length" property is numberOfArgs. // f. Return array. Ok(array.into()) @@ -253,6 +253,16 @@ impl Array { .with_message("array exceeded max size") .into()); } + + // Fast path: + if prototype.is_none() { + return Ok(context + .intrinsics() + .templates() + .array() + .create(ObjectData::array(), vec![JsValue::new(length)])); + } + // 7. Return A. // 2. If proto is not present, set proto to %Array.prototype%. // 3. Let A be ! MakeBasicObject(« [[Prototype]], [[Extensible]] »). @@ -260,7 +270,26 @@ impl Array { // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.2.1. let prototype = prototype.unwrap_or_else(|| context.intrinsics().constructors().array().prototype()); - let array = JsObject::from_proto_and_data(prototype, ObjectData::array()); + + // Fast path: + if context + .intrinsics() + .templates() + .array() + .has_prototype(&prototype) + { + return Ok(context + .intrinsics() + .templates() + .array() + .create(ObjectData::array(), vec![JsValue::new(length)])); + } + + let array = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::array(), + ); // 6. Perform ! OrdinaryDefineOwnProperty(A, "length", PropertyDescriptor { [[Value]]: 𝔽(length), [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }). crate::object::internal_methods::ordinary_define_own_property( @@ -290,27 +319,24 @@ impl Array { { // 1. Assert: elements is a List whose elements are all ECMAScript language values. // 2. Let array be ! ArrayCreate(0). - let array = Self::array_create(0, None, context) - .expect("creating an empty array with the default prototype must not fail"); - // 3. Let n be 0. // 4. For each element e of elements, do // a. Perform ! CreateDataPropertyOrThrow(array, ! ToString(𝔽(n)), e). // b. Set n to n + 1. - // + // 5. Return array. // NOTE: This deviates from the spec, but it should have the same behaviour. let elements: ThinVec<_> = elements.into_iter().collect(); let length = elements.len(); - array - .borrow_mut() - .properties_mut() - .override_indexed_properties(elements); - array - .set(utf16!("length"), length, true, context) - .expect("Should not fail"); - // 5. Return array. - array + context + .intrinsics() + .templates() + .array() + .create_with_indexed_properties( + ObjectData::array(), + vec![JsValue::new(length)], + elements, + ) } /// Utility function for concatenating array objects. diff --git a/boa_engine/src/builtins/array_buffer/mod.rs b/boa_engine/src/builtins/array_buffer/mod.rs index f19e0fc195..6d57ec67e2 100644 --- a/boa_engine/src/builtins/array_buffer/mod.rs +++ b/boa_engine/src/builtins/array_buffer/mod.rs @@ -53,13 +53,11 @@ impl IntrinsicObject for ArrayBuffer { let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); - let get_byte_length = BuiltInBuilder::new(realm) - .callable(Self::get_byte_length) + let get_byte_length = BuiltInBuilder::callable(realm, Self::get_byte_length) .name("get byteLength") .build(); @@ -355,7 +353,8 @@ impl ArrayBuffer { // 3. Set obj.[[ArrayBufferData]] to block. // 4. Set obj.[[ArrayBufferByteLength]] to byteLength. - let obj = JsObject::from_proto_and_data( + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::array_buffer(Self { array_buffer_data: Some(block), diff --git a/boa_engine/src/builtins/boolean/mod.rs b/boa_engine/src/builtins/boolean/mod.rs index 4adebf3c2e..91f8186f56 100644 --- a/boa_engine/src/builtins/boolean/mod.rs +++ b/boa_engine/src/builtins/boolean/mod.rs @@ -68,7 +68,11 @@ impl BuiltInConstructor for Boolean { } let prototype = get_prototype_from_constructor(new_target, StandardConstructors::boolean, context)?; - let boolean = JsObject::from_proto_and_data(prototype, ObjectData::boolean(data)); + let boolean = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::boolean(data), + ); Ok(boolean.into()) } diff --git a/boa_engine/src/builtins/dataview/mod.rs b/boa_engine/src/builtins/dataview/mod.rs index 983fb0c755..56ba390303 100644 --- a/boa_engine/src/builtins/dataview/mod.rs +++ b/boa_engine/src/builtins/dataview/mod.rs @@ -35,18 +35,15 @@ impl IntrinsicObject for DataView { fn init(realm: &Realm) { let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; - let get_buffer = BuiltInBuilder::new(realm) - .callable(Self::get_buffer) + let get_buffer = BuiltInBuilder::callable(realm, Self::get_buffer) .name("get buffer") .build(); - let get_byte_length = BuiltInBuilder::new(realm) - .callable(Self::get_byte_length) + let get_byte_length = BuiltInBuilder::callable(realm, Self::get_byte_length) .name("get byteLength") .build(); - let get_byte_offset = BuiltInBuilder::new(realm) - .callable(Self::get_byte_offset) + let get_byte_offset = BuiltInBuilder::callable(realm, Self::get_byte_offset) .name("get byteOffset") .build(); @@ -194,7 +191,8 @@ impl BuiltInConstructor for DataView { .into()); } - let obj = JsObject::from_proto_and_data( + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::data_view(Self { // 11. Set O.[[ViewedArrayBuffer]] to buffer. diff --git a/boa_engine/src/builtins/date/mod.rs b/boa_engine/src/builtins/date/mod.rs index 764b5b83a4..e9360e1dff 100644 --- a/boa_engine/src/builtins/date/mod.rs +++ b/boa_engine/src/builtins/date/mod.rs @@ -96,14 +96,12 @@ impl IntrinsicObject for Date { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let to_utc_string = BuiltInBuilder::new(realm) - .callable(Self::to_utc_string) + let to_utc_string = BuiltInBuilder::callable(realm, Self::to_utc_string) .name("toUTCString") .length(0) .build(); - let to_primitive = BuiltInBuilder::new(realm) - .callable(Self::to_primitive) + let to_primitive = BuiltInBuilder::callable(realm, Self::to_primitive) .name("[Symbol.toPrimitive]") .length(1) .build(); @@ -289,7 +287,11 @@ impl BuiltInConstructor for Date { get_prototype_from_constructor(new_target, StandardConstructors::date, context)?; // 7. Set O.[[DateValue]] to dv. - let obj = JsObject::from_proto_and_data(prototype, ObjectData::date(dv)); + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::date(dv), + ); // 8. Return O. Ok(obj.into()) diff --git a/boa_engine/src/builtins/error/aggregate.rs b/boa_engine/src/builtins/error/aggregate.rs index 7cdeb8d19f..f9ce1fd70f 100644 --- a/boa_engine/src/builtins/error/aggregate.rs +++ b/boa_engine/src/builtins/error/aggregate.rs @@ -83,7 +83,11 @@ impl BuiltInConstructor for AggregateError { StandardConstructors::aggregate_error, context, )?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Aggregate)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Aggregate), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(1); diff --git a/boa_engine/src/builtins/error/eval.rs b/boa_engine/src/builtins/error/eval.rs index 50c862ad5b..ac3d915ed5 100644 --- a/boa_engine/src/builtins/error/eval.rs +++ b/boa_engine/src/builtins/error/eval.rs @@ -82,7 +82,11 @@ impl BuiltInConstructor for EvalError { // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::eval_error, context)?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Eval)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Eval), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/error/mod.rs b/boa_engine/src/builtins/error/mod.rs index 9444c22aff..0430ddd26f 100644 --- a/boa_engine/src/builtins/error/mod.rs +++ b/boa_engine/src/builtins/error/mod.rs @@ -177,7 +177,11 @@ impl BuiltInConstructor for Error { // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%Error.prototype%", « [[ErrorData]] »). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::error, context)?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Error)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Error), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/error/range.rs b/boa_engine/src/builtins/error/range.rs index 987dc1c36c..36c657b9c6 100644 --- a/boa_engine/src/builtins/error/range.rs +++ b/boa_engine/src/builtins/error/range.rs @@ -80,7 +80,11 @@ impl BuiltInConstructor for RangeError { // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::range_error, context)?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Range)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Range), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/error/reference.rs b/boa_engine/src/builtins/error/reference.rs index 5b7b31f74e..e72dd09883 100644 --- a/boa_engine/src/builtins/error/reference.rs +++ b/boa_engine/src/builtins/error/reference.rs @@ -82,7 +82,11 @@ impl BuiltInConstructor for ReferenceError { StandardConstructors::reference_error, context, )?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Reference)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Reference), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/error/syntax.rs b/boa_engine/src/builtins/error/syntax.rs index 42f83997bc..444e32a3d7 100644 --- a/boa_engine/src/builtins/error/syntax.rs +++ b/boa_engine/src/builtins/error/syntax.rs @@ -85,7 +85,11 @@ impl BuiltInConstructor for SyntaxError { StandardConstructors::syntax_error, context, )?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Syntax)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Syntax), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/error/type.rs b/boa_engine/src/builtins/error/type.rs index b2a480f78f..87cdc6fae5 100644 --- a/boa_engine/src/builtins/error/type.rs +++ b/boa_engine/src/builtins/error/type.rs @@ -90,7 +90,11 @@ impl BuiltInConstructor for TypeError { // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::type_error, context)?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Type)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Type), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/error/uri.rs b/boa_engine/src/builtins/error/uri.rs index 94794d5d13..34a7514d6c 100644 --- a/boa_engine/src/builtins/error/uri.rs +++ b/boa_engine/src/builtins/error/uri.rs @@ -81,7 +81,11 @@ impl BuiltInConstructor for UriError { // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::uri_error, context)?; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(ErrorKind::Uri)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(ErrorKind::Uri), + ); // 3. If message is not undefined, then let message = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/escape/mod.rs b/boa_engine/src/builtins/escape/mod.rs index 1d6632ab4f..a889714207 100644 --- a/boa_engine/src/builtins/escape/mod.rs +++ b/boa_engine/src/builtins/escape/mod.rs @@ -23,8 +23,7 @@ pub(crate) struct Escape; impl IntrinsicObject for Escape { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(escape) + BuiltInBuilder::callable_with_intrinsic::(realm, escape) .name(Self::NAME) .length(1) .build(); @@ -96,8 +95,7 @@ pub(crate) struct Unescape; impl IntrinsicObject for Unescape { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(unescape) + BuiltInBuilder::callable_with_intrinsic::(realm, unescape) .name(Self::NAME) .length(1) .build(); diff --git a/boa_engine/src/builtins/eval/mod.rs b/boa_engine/src/builtins/eval/mod.rs index 070459e9d2..f0676e99b7 100644 --- a/boa_engine/src/builtins/eval/mod.rs +++ b/boa_engine/src/builtins/eval/mod.rs @@ -31,8 +31,7 @@ impl IntrinsicObject for Eval { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::with_intrinsic::(realm) - .callable(Self::eval) + BuiltInBuilder::callable_with_intrinsic::(realm, Self::eval) .name(Self::NAME) .length(1) .build(); diff --git a/boa_engine/src/builtins/function/arguments.rs b/boa_engine/src/builtins/function/arguments.rs index 40b4c97acd..b882a5715f 100644 --- a/boa_engine/src/builtins/function/arguments.rs +++ b/boa_engine/src/builtins/function/arguments.rs @@ -1,9 +1,6 @@ use crate::{ environments::DeclarativeEnvironment, object::{JsObject, ObjectData}, - property::PropertyDescriptor, - string::utf16, - symbol::{self, JsSymbol}, Context, JsValue, }; use boa_ast::{function::FormalParameterList, operations::bound_names}; @@ -77,68 +74,42 @@ impl Arguments { // 1. Let len be the number of elements in argumentsList. let len = arguments_list.len(); + let values_function = context.intrinsics().objects().array_prototype_values(); + let throw_type_error = context.intrinsics().objects().throw_type_error(); + // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%, « [[ParameterMap]] »). // 3. Set obj.[[ParameterMap]] to undefined. // skipped because the `Arguments` enum ensures ordinary argument objects don't have a `[[ParameterMap]]` - let obj = JsObject::from_proto_and_data( - context.intrinsics().constructors().object().prototype(), - ObjectData::arguments(Self::Unmapped), - ); - - // 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), - // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). - obj.define_property_or_throw( - utf16!("length"), - PropertyDescriptor::builder() - .value(len) - .writable(true) - .enumerable(false) - .configurable(true), - context, - ) - .expect("Defining new own properties for a new ordinary object cannot fail"); + let obj = context + .intrinsics() + .templates() + .unmapped_arguments() + .create( + ObjectData::arguments(Self::Unmapped), + vec![ + // 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + len.into(), + // 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + values_function.into(), + // 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, + // [[Configurable]]: false }). + throw_type_error.clone().into(), // get + throw_type_error.into(), // set + ], + ); // 5. Let index be 0. // 6. Repeat, while index < len, - for (index, value) in arguments_list.iter().cloned().enumerate() { - // a. Let val be argumentsList[index]. - // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). - obj.create_data_property_or_throw(index, value, context) - .expect("Defining new own properties for a new ordinary object cannot fail"); - - // c. Set index to index + 1. - } - - // 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { - // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, - // [[Configurable]]: true }). - let values_function = context.intrinsics().objects().array_prototype_values(); - obj.define_property_or_throw( - symbol::JsSymbol::iterator(), - PropertyDescriptor::builder() - .value(values_function) - .writable(true) - .enumerable(false) - .configurable(true), - context, - ) - .expect("Defining new own properties for a new ordinary object cannot fail"); - - let throw_type_error = context.intrinsics().objects().throw_type_error(); - - // 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { - // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, - // [[Configurable]]: false }). - obj.define_property_or_throw( - utf16!("callee"), - PropertyDescriptor::builder() - .get(throw_type_error.clone()) - .set(throw_type_error) - .enumerable(false) - .configurable(false), - context, - ) - .expect("Defining new own properties for a new ordinary object cannot fail"); + // a. Let val be argumentsList[index]. + // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). + // c. Set index to index + 1. + obj.borrow_mut() + .properties_mut() + .override_indexed_properties(arguments_list.iter().cloned().collect()); // 9. Return obj. obj @@ -222,71 +193,36 @@ impl Arguments { map.binding_indices[*property_index] = Some(*binding_index); } + // %Array.prototype.values% + let values_function = context.intrinsics().objects().array_prototype_values(); + // 11. Set obj.[[ParameterMap]] to map. - let obj = JsObject::from_proto_and_data( - context.intrinsics().constructors().object().prototype(), + let obj = context.intrinsics().templates().mapped_arguments().create( ObjectData::arguments(Self::Mapped(map)), + vec![ + // 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + len.into(), + // 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + values_function.into(), + // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + func.clone().into(), + ], ); // 14. Let index be 0. // 15. Repeat, while index < len, - for (index, val) in arguments_list.iter().cloned().enumerate() { - // a. Let val be argumentsList[index]. - // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). - // Note: Insert is used here because `CreateDataPropertyOrThrow` would cause a panic while executing - // exotic argument object set methods before the variables in the environment are initialized. - obj.insert( - index, - PropertyDescriptor::builder() - .value(val) - .writable(true) - .enumerable(true) - .configurable(true) - .build(), - ); - // c. Set index to index + 1. - } - - // 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), - // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). - obj.define_property_or_throw( - utf16!("length"), - PropertyDescriptor::builder() - .value(len) - .writable(true) - .enumerable(false) - .configurable(true), - context, - ) - .expect("Defining new own properties for a new ordinary object cannot fail"); - - // 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { - // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, - // [[Configurable]]: true }). - let values_function = context.intrinsics().objects().array_prototype_values(); - obj.define_property_or_throw( - JsSymbol::iterator(), - PropertyDescriptor::builder() - .value(values_function) - .writable(true) - .enumerable(false) - .configurable(true), - context, - ) - .expect("Defining new own properties for a new ordinary object cannot fail"); - - // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { - // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). - obj.define_property_or_throw( - utf16!("callee"), - PropertyDescriptor::builder() - .value(func.clone()) - .writable(true) - .enumerable(false) - .configurable(true), - context, - ) - .expect("Defining new own properties for a new ordinary object cannot fail"); + // a. Let val be argumentsList[index]. + // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). + // Note: Direct initialization of indexed array is used here because `CreateDataPropertyOrThrow` + // would cause a panic while executing exotic argument object set methods before the variables + // in the environment are initialized. + obj.borrow_mut() + .properties_mut() + .override_indexed_properties(arguments_list.iter().cloned().collect()); // 22. Return obj. obj diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index ef0eae5360..dab238ef99 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -442,17 +442,7 @@ impl IntrinsicObject for BuiltInFunctionObject { fn init(realm: &Realm) { let _timer = Profiler::global().start_event("function", "init"); - BuiltInBuilder::with_object( - realm, - realm.intrinsics().constructors().function().prototype(), - ) - .callable(Self::prototype) - .name("") - .length(0) - .build(); - - let has_instance = BuiltInBuilder::new(realm) - .callable(Self::has_instance) + let has_instance = BuiltInBuilder::callable(realm, Self::has_instance) .name("[Symbol.iterator]") .length(1) .build(); @@ -478,6 +468,15 @@ impl IntrinsicObject for BuiltInFunctionObject { Attribute::CONFIGURABLE, ) .build(); + + let prototype = realm.intrinsics().constructors().function().prototype(); + + BuiltInBuilder::callable_with_object(realm, prototype.clone(), Self::prototype) + .name("") + .length(0) + .build(); + + prototype.set_prototype(Some(realm.intrinsics().constructors().object().prototype())); } fn get(intrinsics: &Intrinsics) -> JsObject { @@ -1098,7 +1097,8 @@ impl BoundFunction { // 8. Set obj.[[BoundThis]] to boundThis. // 9. Set obj.[[BoundArguments]] to boundArgs. // 10. Return obj. - Ok(JsObject::from_proto_and_data( + Ok(JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), proto, ObjectData::bound_function( Self { diff --git a/boa_engine/src/builtins/intl/collator/mod.rs b/boa_engine/src/builtins/intl/collator/mod.rs index dcbed041fb..af88d1cb30 100644 --- a/boa_engine/src/builtins/intl/collator/mod.rs +++ b/boa_engine/src/builtins/intl/collator/mod.rs @@ -163,8 +163,7 @@ impl IntrinsicObject for Collator { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let compare = BuiltInBuilder::new(realm) - .callable(Self::compare) + let compare = BuiltInBuilder::callable(realm, Self::compare) .name("get compare") .build(); @@ -364,7 +363,8 @@ impl BuiltInConstructor for Collator { let prototype = get_prototype_from_constructor(new_target, StandardConstructors::collator, context)?; - let collator = JsObject::from_proto_and_data( + let collator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::collator(Self { locale, @@ -509,10 +509,11 @@ impl Collator { })?; // 3. Let options be OrdinaryObjectCreate(%Object.prototype%). - let options = JsObject::from_proto_and_data( - context.intrinsics().constructors().object().prototype(), - ObjectData::ordinary(), - ); + let options = context + .intrinsics() + .templates() + .ordinary_object() + .create(ObjectData::ordinary(), vec![]); // 4. For each row of Table 4, except the header row, in table order, do // a. Let p be the Property value of the current row. diff --git a/boa_engine/src/builtins/intl/date_time_format.rs b/boa_engine/src/builtins/intl/date_time_format.rs index eb4c4f8021..6bb4e8c505 100644 --- a/boa_engine/src/builtins/intl/date_time_format.rs +++ b/boa_engine/src/builtins/intl/date_time_format.rs @@ -122,7 +122,8 @@ impl BuiltInConstructor for DateTimeFormat { // « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]], // [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]], // [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »). - let date_time_format = JsObject::from_proto_and_data( + let date_time_format = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::date_time_format(Box::new(Self { initialized_date_time_format: true, @@ -192,7 +193,11 @@ pub(crate) fn to_date_time_options( } else { Some(options.to_object(context)?) }; - let options = JsObject::from_proto_and_data(options, ObjectData::ordinary()); + let options = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + options, + ObjectData::ordinary(), + ); // 3. Let needDefaults be true. let mut need_defaults = true; diff --git a/boa_engine/src/builtins/intl/list_format/mod.rs b/boa_engine/src/builtins/intl/list_format/mod.rs index 656699338a..e17054e57d 100644 --- a/boa_engine/src/builtins/intl/list_format/mod.rs +++ b/boa_engine/src/builtins/intl/list_format/mod.rs @@ -147,7 +147,8 @@ impl BuiltInConstructor for ListFormat { // 2. Let listFormat be ? OrdinaryCreateFromConstructor(NewTarget, "%ListFormat.prototype%", « [[InitializedListFormat]], [[Locale]], [[Type]], [[Style]], [[Templates]] »). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::list_format, context)?; - let list_format = JsObject::from_proto_and_data( + let list_format = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::list_format(Self { formatter: context @@ -358,10 +359,11 @@ impl ListFormat { // 4. For each Record { [[Type]], [[Value]] } part in parts, do for (n, part) in parts.0.into_iter().enumerate() { // a. Let O be OrdinaryObjectCreate(%Object.prototype%). - let o = JsObject::from_proto_and_data( - context.intrinsics().constructors().object().prototype(), - ObjectData::ordinary(), - ); + let o = context + .intrinsics() + .templates() + .ordinary_object() + .create(ObjectData::ordinary(), vec![]); // b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]). o.create_data_property_or_throw(utf16!("type"), part.typ(), context) @@ -410,10 +412,11 @@ impl ListFormat { })?; // 3. Let options be OrdinaryObjectCreate(%Object.prototype%). - let options = JsObject::from_proto_and_data( - context.intrinsics().constructors().object().prototype(), - ObjectData::ordinary(), - ); + let options = context + .intrinsics() + .templates() + .ordinary_object() + .create(ObjectData::ordinary(), vec![]); // 4. For each row of Table 11, except the header row, in table order, do // a. Let p be the Property value of the current row. diff --git a/boa_engine/src/builtins/intl/locale/mod.rs b/boa_engine/src/builtins/intl/locale/mod.rs index 2449ca930e..f8ee0809cf 100644 --- a/boa_engine/src/builtins/intl/locale/mod.rs +++ b/boa_engine/src/builtins/intl/locale/mod.rs @@ -35,53 +35,43 @@ impl IntrinsicObject for Locale { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let base_name = BuiltInBuilder::new(realm) - .callable(Self::base_name) + let base_name = BuiltInBuilder::callable(realm, Self::base_name) .name("get baseName") .build(); - let calendar = BuiltInBuilder::new(realm) - .callable(Self::calendar) + let calendar = BuiltInBuilder::callable(realm, Self::calendar) .name("get calendar") .build(); - let case_first = BuiltInBuilder::new(realm) - .callable(Self::case_first) + let case_first = BuiltInBuilder::callable(realm, Self::case_first) .name("get caseFirst") .build(); - let collation = BuiltInBuilder::new(realm) - .callable(Self::collation) + let collation = BuiltInBuilder::callable(realm, Self::collation) .name("get collation") .build(); - let hour_cycle = BuiltInBuilder::new(realm) - .callable(Self::hour_cycle) + let hour_cycle = BuiltInBuilder::callable(realm, Self::hour_cycle) .name("get hourCycle") .build(); - let numeric = BuiltInBuilder::new(realm) - .callable(Self::numeric) + let numeric = BuiltInBuilder::callable(realm, Self::numeric) .name("get numeric") .build(); - let numbering_system = BuiltInBuilder::new(realm) - .callable(Self::numbering_system) + let numbering_system = BuiltInBuilder::callable(realm, Self::numbering_system) .name("get numberingSystem") .build(); - let language = BuiltInBuilder::new(realm) - .callable(Self::language) + let language = BuiltInBuilder::callable(realm, Self::language) .name("get language") .build(); - let script = BuiltInBuilder::new(realm) - .callable(Self::script) + let script = BuiltInBuilder::callable(realm, Self::script) .name("get script") .build(); - let region = BuiltInBuilder::new(realm) - .callable(Self::region) + let region = BuiltInBuilder::callable(realm, Self::region) .name("get region") .build(); @@ -391,7 +381,11 @@ impl BuiltInConstructor for Locale { // 6. Let locale be ? OrdinaryCreateFromConstructor(NewTarget, "%Locale.prototype%", internalSlotsList). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::locale, context)?; - let locale = JsObject::from_proto_and_data(prototype, ObjectData::locale(tag)); + let locale = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::locale(tag), + ); // 37. Return locale. Ok(locale.into()) @@ -429,7 +423,12 @@ impl Locale { // 4. Return ! Construct(%Locale%, maximal). let prototype = context.intrinsics().constructors().locale().prototype(); - Ok(JsObject::from_proto_and_data(prototype, ObjectData::locale(loc)).into()) + Ok(JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::locale(loc), + ) + .into()) } /// [`Intl.Locale.prototype.minimize ( )`][spec] @@ -462,7 +461,12 @@ impl Locale { // 4. Return ! Construct(%Locale%, minimal). let prototype = context.intrinsics().constructors().locale().prototype(); - Ok(JsObject::from_proto_and_data(prototype, ObjectData::locale(loc)).into()) + Ok(JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::locale(loc), + ) + .into()) } /// [`Intl.Locale.prototype.toString ( )`][spec]. diff --git a/boa_engine/src/builtins/intl/options.rs b/boa_engine/src/builtins/intl/options.rs index e08a949b75..603a81722e 100644 --- a/boa_engine/src/builtins/intl/options.rs +++ b/boa_engine/src/builtins/intl/options.rs @@ -244,7 +244,11 @@ pub(super) fn coerce_options_to_object( // If options is undefined, then if options.is_undefined() { // a. Return OrdinaryObjectCreate(null). - return Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary())); + return Ok(JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + None, + ObjectData::ordinary(), + )); } // 2. Return ? ToObject(options). diff --git a/boa_engine/src/builtins/intl/segmenter/iterator.rs b/boa_engine/src/builtins/intl/segmenter/iterator.rs index 7b3601e407..18dca85131 100644 --- a/boa_engine/src/builtins/intl/segmenter/iterator.rs +++ b/boa_engine/src/builtins/intl/segmenter/iterator.rs @@ -87,7 +87,8 @@ impl SegmentIterator { // 4. Set iterator.[[IteratedString]] to string. // 5. Set iterator.[[IteratedStringNextSegmentCodeUnitIndex]] to 0. // 6. Return iterator. - JsObject::from_proto_and_data( + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context .intrinsics() .objects() diff --git a/boa_engine/src/builtins/intl/segmenter/mod.rs b/boa_engine/src/builtins/intl/segmenter/mod.rs index f3fad7f25f..ed204e075f 100644 --- a/boa_engine/src/builtins/intl/segmenter/mod.rs +++ b/boa_engine/src/builtins/intl/segmenter/mod.rs @@ -163,7 +163,11 @@ impl BuiltInConstructor for Segmenter { let proto = get_prototype_from_constructor(new_target, StandardConstructors::segmenter, context)?; - let segmenter = JsObject::from_proto_and_data(proto, ObjectData::segmenter(segmenter)); + let segmenter = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + proto, + ObjectData::segmenter(segmenter), + ); // 14. Return segmenter. Ok(segmenter.into()) diff --git a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs index 8f11666b1d..5557b7427d 100644 --- a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs +++ b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs @@ -65,7 +65,8 @@ impl AsyncFromSyncIterator { ) -> IteratorRecord { // 1. Let asyncIterator be OrdinaryObjectCreate(%AsyncFromSyncIteratorPrototype%, « [[SyncIteratorRecord]] »). // 2. Set asyncIterator.[[SyncIteratorRecord]] to syncIteratorRecord. - let async_iterator = JsObject::from_proto_and_data( + let async_iterator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context .intrinsics() .objects() diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index 48e498d6a3..e6dddab4da 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -5,7 +5,7 @@ use crate::{ context::intrinsics::Intrinsics, error::JsNativeError, js_string, - object::JsObject, + object::{JsObject, ObjectData}, realm::Realm, symbol::JsSymbol, Context, JsResult, JsValue, @@ -201,14 +201,14 @@ pub fn create_iter_result_object(value: JsValue, done: bool, context: &mut Conte // 1. Assert: Type(done) is Boolean. // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%). - let obj = JsObject::with_object_proto(context.intrinsics()); - // 3. Perform ! CreateDataPropertyOrThrow(obj, "value", value). - obj.create_data_property_or_throw(js_string!("value"), value, context) - .expect("this CreateDataPropertyOrThrow call must not fail"); // 4. Perform ! CreateDataPropertyOrThrow(obj, "done", done). - obj.create_data_property_or_throw(js_string!("done"), done, context) - .expect("this CreateDataPropertyOrThrow call must not fail"); + let obj = context + .intrinsics() + .templates() + .iterator_result() + .create(ObjectData::ordinary(), vec![value, done.into()]); + // 5. Return obj. obj.into() } diff --git a/boa_engine/src/builtins/map/map_iterator.rs b/boa_engine/src/builtins/map/map_iterator.rs index 1092d8c585..4d531d5bfc 100644 --- a/boa_engine/src/builtins/map/map_iterator.rs +++ b/boa_engine/src/builtins/map/map_iterator.rs @@ -85,7 +85,8 @@ impl MapIterator { map_iteration_kind: kind, lock, }; - let map_iterator = JsObject::from_proto_and_data( + let map_iterator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().objects().iterator_prototypes().map(), ObjectData::map_iterator(iter), ); diff --git a/boa_engine/src/builtins/map/mod.rs b/boa_engine/src/builtins/map/mod.rs index c321576c7c..f8d04e4fc1 100644 --- a/boa_engine/src/builtins/map/mod.rs +++ b/boa_engine/src/builtins/map/mod.rs @@ -41,18 +41,15 @@ impl IntrinsicObject for Map { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); - let get_size = BuiltInBuilder::new(realm) - .callable(Self::get_size) + let get_size = BuiltInBuilder::callable(realm, Self::get_size) .name("get size") .build(); - let entries_function = BuiltInBuilder::new(realm) - .callable(Self::entries) + let entries_function = BuiltInBuilder::callable(realm, Self::entries) .name("entries") .build(); @@ -136,7 +133,11 @@ impl BuiltInConstructor for Map { // 3. Set map.[[MapData]] to a new empty List. let prototype = get_prototype_from_constructor(new_target, StandardConstructors::map, context)?; - let map = JsObject::from_proto_and_data(prototype, ObjectData::map(OrderedMap::new())); + let map = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::map(OrderedMap::new()), + ); // 4. If iterable is either undefined or null, return map. let iterable = match args.get_or_undefined(0) { diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index c6ad308cfa..e726d54a01 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -95,8 +95,9 @@ use crate::{ js_string, native_function::{NativeFunction, NativeFunctionPointer}, object::{ - FunctionBinding, JsFunction, JsObject, JsPrototype, Object, ObjectData, CONSTRUCTOR, - PROTOTYPE, + shape::{property_table::PropertyTableInner, slot::SlotAttributes}, + FunctionBinding, JsFunction, JsObject, JsPrototype, Object, ObjectData, ObjectKind, + CONSTRUCTOR, PROTOTYPE, }, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, @@ -584,17 +585,17 @@ struct BuiltInBuilder<'ctx, Kind> { } impl<'ctx> BuiltInBuilder<'ctx, OrdinaryObject> { - fn new(realm: &'ctx Realm) -> BuiltInBuilder<'ctx, OrdinaryObject> { - BuiltInBuilder { - realm, - object: BuiltInObjectInitializer::Unique { - object: Object::default(), - data: ObjectData::ordinary(), - }, - kind: OrdinaryObject, - prototype: realm.intrinsics().constructors().object().prototype(), - } - } + // fn new(realm: &'ctx Realm) -> BuiltInBuilder<'ctx, OrdinaryObject> { + // BuiltInBuilder { + // realm, + // object: BuiltInObjectInitializer::Unique { + // object: Object::default(), + // data: ObjectData::ordinary(), + // }, + // kind: OrdinaryObject, + // prototype: realm.intrinsics().constructors().object().prototype(), + // } + // } fn with_intrinsic( realm: &'ctx Realm, @@ -606,82 +607,45 @@ impl<'ctx> BuiltInBuilder<'ctx, OrdinaryObject> { prototype: realm.intrinsics().constructors().object().prototype(), } } - - fn with_object(realm: &'ctx Realm, object: JsObject) -> BuiltInBuilder<'ctx, OrdinaryObject> { - BuiltInBuilder { - realm, - object: BuiltInObjectInitializer::Shared(object), - kind: OrdinaryObject, - prototype: realm.intrinsics().constructors().object().prototype(), - } - } } -impl<'ctx> BuiltInBuilder<'ctx, OrdinaryObject> { - fn callable( - self, - function: NativeFunctionPointer, - ) -> BuiltInBuilder<'ctx, Callable> { - BuiltInBuilder { - realm: self.realm, - object: self.object, - kind: Callable { - function, - name: js_string!(""), - length: 0, - kind: OrdinaryFunction, - realm: self.realm.clone(), - }, - prototype: self - .realm - .intrinsics() - .constructors() - .function() - .prototype(), - } - } +struct BuiltInConstructorWithPrototype<'ctx> { + realm: &'ctx Realm, + function: NativeFunctionPointer, + name: JsString, + length: usize, + + object_property_table: PropertyTableInner, + object_storage: Vec, + object: JsObject, + + prototype_property_table: PropertyTableInner, + prototype_storage: Vec, + prototype: JsObject, + __proto__: JsPrototype, + inherits: Option, + attributes: Attribute, } -impl<'ctx> BuiltInBuilder<'ctx, Callable> { - fn from_standard_constructor( - realm: &'ctx Realm, - ) -> BuiltInBuilder<'ctx, Callable> { - let constructor = SC::STANDARD_CONSTRUCTOR(realm.intrinsics().constructors()); - BuiltInBuilder { - realm, - object: BuiltInObjectInitializer::Shared(constructor.constructor()), - kind: Callable { - function: SC::constructor, - name: js_string!(SC::NAME), - length: SC::LENGTH, - kind: Constructor { - prototype: constructor.prototype(), - inherits: Some(realm.intrinsics().constructors().object().prototype()), - attributes: Attribute::WRITABLE | Attribute::CONFIGURABLE, - }, - realm: realm.clone(), - }, - prototype: realm.intrinsics().constructors().function().prototype(), - } +#[allow(dead_code)] +impl BuiltInConstructorWithPrototype<'_> { + /// Specify how many arguments the constructor function takes. + /// + /// Default is `0`. + #[inline] + const fn length(mut self, length: usize) -> Self { + self.length = length; + self } - fn no_proto(self) -> BuiltInBuilder<'ctx, Callable> { - BuiltInBuilder { - realm: self.realm, - object: self.object, - kind: Callable { - function: self.kind.function, - name: self.kind.name, - length: self.kind.length, - kind: ConstructorNoProto, - realm: self.realm.clone(), - }, - prototype: self.prototype, - } + /// Specify the name of the constructor function. + /// + /// Default is `""` + fn name>(mut self, name: N) -> Self { + self.name = name.into(); + self } -} -impl BuiltInBuilder<'_, T> { /// Adds a new static method to the builtin object. fn static_method( mut self, @@ -693,20 +657,21 @@ impl BuiltInBuilder<'_, T> { B: Into, { let binding = binding.into(); - let function = BuiltInBuilder::new(self.realm) - .callable(function) + let function = BuiltInBuilder::callable(self.realm, function) .name(binding.name) .length(length) .build(); - self.object.insert( + debug_assert!(self + .object_property_table + .map + .get(&binding.binding) + .is_none()); + self.object_property_table.insert( binding.binding, - PropertyDescriptor::builder() - .value(function) - .writable(true) - .enumerable(false) - .configurable(true), + SlotAttributes::WRITABLE | SlotAttributes::CONFIGURABLE, ); + self.object_storage.push(function.into()); self } @@ -716,12 +681,12 @@ impl BuiltInBuilder<'_, T> { K: Into, V: Into, { - let property = PropertyDescriptor::builder() - .value(value) - .writable(attribute.writable()) - .enumerable(attribute.enumerable()) - .configurable(attribute.configurable()); - self.object.insert(key, property); + let key = key.into(); + + debug_assert!(self.object_property_table.map.get(&key).is_none()); + self.object_property_table + .insert(key, SlotAttributes::from_bits_truncate(attribute.bits())); + self.object_storage.push(value.into()); self } @@ -736,12 +701,19 @@ impl BuiltInBuilder<'_, T> { where K: Into, { - let property = PropertyDescriptor::builder() - .maybe_get(get) - .maybe_set(set) - .enumerable(attribute.enumerable()) - .configurable(attribute.configurable()); - self.object.insert(key, property); + let mut attributes = SlotAttributes::from_bits_truncate(attribute.bits()); + debug_assert!(!attributes.contains(SlotAttributes::WRITABLE)); + attributes.set(SlotAttributes::GET, get.is_some()); + attributes.set(SlotAttributes::SET, set.is_some()); + + let key = key.into(); + + debug_assert!(self.object_property_table.map.get(&key).is_none()); + self.object_property_table.insert(key, attributes); + self.object_storage.extend([ + get.map(JsValue::new).unwrap_or_default(), + set.map(JsValue::new).unwrap_or_default(), + ]); self } @@ -749,53 +721,52 @@ impl BuiltInBuilder<'_, T> { /// /// Default is `Function.prototype` for constructors and `Object.prototype` for statics. fn prototype(mut self, prototype: JsObject) -> Self { - self.prototype = prototype; + self.__proto__ = Some(prototype); self } -} -impl BuiltInBuilder<'_, Callable> { /// Adds a new method to the constructor's prototype. - fn method(self, function: NativeFunctionPointer, binding: B, length: usize) -> Self + fn method(mut self, function: NativeFunctionPointer, binding: B, length: usize) -> Self where B: Into, { let binding = binding.into(); - let function = BuiltInBuilder::new(self.realm) - .callable(function) + let function = BuiltInBuilder::callable(self.realm, function) .name(binding.name) .length(length) .build(); - self.kind.kind.prototype.borrow_mut().insert( + debug_assert!(self + .prototype_property_table + .map + .get(&binding.binding) + .is_none()); + self.prototype_property_table.insert( binding.binding, - PropertyDescriptor::builder() - .value(function) - .writable(true) - .enumerable(false) - .configurable(true), + SlotAttributes::WRITABLE | SlotAttributes::CONFIGURABLE, ); + self.prototype_storage.push(function.into()); self } /// Adds a new data property to the constructor's prototype. - fn property(self, key: K, value: V, attribute: Attribute) -> Self + fn property(mut self, key: K, value: V, attribute: Attribute) -> Self where K: Into, V: Into, { - let property = PropertyDescriptor::builder() - .value(value) - .writable(attribute.writable()) - .enumerable(attribute.enumerable()) - .configurable(attribute.configurable()); - self.kind.kind.prototype.borrow_mut().insert(key, property); + let key = key.into(); + + debug_assert!(self.prototype_property_table.map.get(&key).is_none()); + self.prototype_property_table + .insert(key, SlotAttributes::from_bits_truncate(attribute.bits())); + self.prototype_storage.push(value.into()); self } /// Adds new accessor property to the constructor's prototype. fn accessor( - self, + mut self, key: K, get: Option, set: Option, @@ -804,12 +775,19 @@ impl BuiltInBuilder<'_, Callable> { where K: Into, { - let property = PropertyDescriptor::builder() - .maybe_get(get) - .maybe_set(set) - .enumerable(attribute.enumerable()) - .configurable(attribute.configurable()); - self.kind.kind.prototype.borrow_mut().insert(key, property); + let mut attributes = SlotAttributes::from_bits_truncate(attribute.bits()); + debug_assert!(!attributes.contains(SlotAttributes::WRITABLE)); + attributes.set(SlotAttributes::GET, get.is_some()); + attributes.set(SlotAttributes::SET, set.is_some()); + + let key = key.into(); + + debug_assert!(self.prototype_property_table.map.get(&key).is_none()); + self.prototype_property_table.insert(key, attributes); + self.prototype_storage.extend([ + get.map(JsValue::new).unwrap_or_default(), + set.map(JsValue::new).unwrap_or_default(), + ]); self } @@ -818,13 +796,259 @@ impl BuiltInBuilder<'_, Callable> { /// Default is `Object.prototype`. #[allow(clippy::missing_const_for_fn)] fn inherits(mut self, prototype: JsPrototype) -> Self { - self.kind.kind.inherits = prototype; + self.inherits = prototype; self } /// Specifies the property attributes of the prototype's "constructor" property. const fn constructor_attributes(mut self, attributes: Attribute) -> Self { - self.kind.kind.attributes = attributes; + self.attributes = attributes; + self + } + + fn build(mut self) { + let function = function::Function::new( + function::FunctionKind::Native { + function: NativeFunction::from_fn_ptr(self.function), + constructor: (true).then_some(function::ConstructorKind::Base), + }, + self.realm.clone(), + ); + + let length = self.length; + let name = self.name.clone(); + let prototype = self.prototype.clone(); + self = self.static_property("length", length, Attribute::CONFIGURABLE); + self = self.static_property("name", name, Attribute::CONFIGURABLE); + self = self.static_property(PROTOTYPE, prototype, Attribute::empty()); + + let attributes = self.attributes; + let object = self.object.clone(); + self = self.property(CONSTRUCTOR, object, attributes); + + { + let mut prototype = self.prototype.borrow_mut(); + prototype + .properties_mut() + .shape + .as_unique() + .expect("The object should have a unique shape") + .override_internal(self.prototype_property_table, self.inherits); + + let prototype_old_storage = std::mem::replace( + &mut prototype.properties_mut().storage, + self.prototype_storage, + ); + + debug_assert_eq!(prototype_old_storage.len(), 0); + } + + let mut object = self.object.borrow_mut(); + *object.kind_mut() = ObjectKind::Function(function); + object + .properties_mut() + .shape + .as_unique() + .expect("The object should have a unique shape") + .override_internal(self.object_property_table, self.__proto__); + + let object_old_storage = + std::mem::replace(&mut object.properties_mut().storage, self.object_storage); + + debug_assert_eq!(object_old_storage.len(), 0); + } + + fn build_without_prototype(mut self) { + let function = function::Function::new( + function::FunctionKind::Native { + function: NativeFunction::from_fn_ptr(self.function), + constructor: (true).then_some(function::ConstructorKind::Base), + }, + self.realm.clone(), + ); + + let length = self.length; + let name = self.name.clone(); + self = self.static_property("length", length, Attribute::CONFIGURABLE); + self = self.static_property("name", name, Attribute::CONFIGURABLE); + + let mut object = self.object.borrow_mut(); + *object.kind_mut() = ObjectKind::Function(function); + object + .properties_mut() + .shape + .as_unique() + .expect("The object should have a unique shape") + .override_internal(self.object_property_table, self.__proto__); + + let object_old_storage = + std::mem::replace(&mut object.properties_mut().storage, self.object_storage); + + debug_assert_eq!(object_old_storage.len(), 0); + } +} + +struct BuiltInCallable<'ctx> { + realm: &'ctx Realm, + function: NativeFunctionPointer, + name: JsString, + length: usize, +} + +impl BuiltInCallable<'_> { + /// Specify how many arguments the constructor function takes. + /// + /// Default is `0`. + #[inline] + const fn length(mut self, length: usize) -> Self { + self.length = length; + self + } + + /// Specify the name of the constructor function. + /// + /// Default is `""` + fn name>(mut self, name: N) -> Self { + self.name = name.into(); + self + } + + fn build(self) -> JsFunction { + let function = function::FunctionKind::Native { + function: NativeFunction::from_fn_ptr(self.function), + constructor: None, + }; + + let function = function::Function::new(function, self.realm.clone()); + + let object = self.realm.intrinsics().templates().function().create( + ObjectData::function(function), + vec![JsValue::new(self.length), JsValue::new(self.name)], + ); + + JsFunction::from_object_unchecked(object) + } +} + +impl<'ctx> BuiltInBuilder<'ctx, OrdinaryObject> { + fn callable(realm: &'ctx Realm, function: NativeFunctionPointer) -> BuiltInCallable<'ctx> { + BuiltInCallable { + realm, + function, + length: 0, + name: js_string!(""), + } + } + + fn callable_with_intrinsic( + realm: &'ctx Realm, + function: NativeFunctionPointer, + ) -> BuiltInBuilder<'ctx, Callable> { + BuiltInBuilder { + realm, + object: BuiltInObjectInitializer::Shared(I::get(realm.intrinsics())), + kind: Callable { + function, + name: js_string!(""), + length: 0, + kind: OrdinaryFunction, + realm: realm.clone(), + }, + prototype: realm.intrinsics().constructors().function().prototype(), + } + } + + fn callable_with_object( + realm: &'ctx Realm, + object: JsObject, + function: NativeFunctionPointer, + ) -> BuiltInBuilder<'ctx, Callable> { + BuiltInBuilder { + realm, + object: BuiltInObjectInitializer::Shared(object), + kind: Callable { + function, + name: js_string!(""), + length: 0, + kind: OrdinaryFunction, + realm: realm.clone(), + }, + prototype: realm.intrinsics().constructors().function().prototype(), + } + } +} + +impl<'ctx> BuiltInBuilder<'ctx, Callable> { + fn from_standard_constructor( + realm: &'ctx Realm, + ) -> BuiltInConstructorWithPrototype<'ctx> { + let constructor = SC::STANDARD_CONSTRUCTOR(realm.intrinsics().constructors()); + BuiltInConstructorWithPrototype { + realm, + function: SC::constructor, + name: js_string!(SC::NAME), + length: SC::LENGTH, + object_property_table: PropertyTableInner::default(), + object_storage: Vec::default(), + object: constructor.constructor(), + prototype_property_table: PropertyTableInner::default(), + prototype_storage: Vec::default(), + prototype: constructor.prototype(), + __proto__: Some(realm.intrinsics().constructors().function().prototype()), + inherits: Some(realm.intrinsics().constructors().object().prototype()), + attributes: Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + } + } +} + +impl BuiltInBuilder<'_, T> { + /// Adds a new static method to the builtin object. + fn static_method( + mut self, + function: NativeFunctionPointer, + binding: B, + length: usize, + ) -> Self + where + B: Into, + { + let binding = binding.into(); + let function = BuiltInBuilder::callable(self.realm, function) + .name(binding.name) + .length(length) + .build(); + + self.object.insert( + binding.binding, + PropertyDescriptor::builder() + .value(function) + .writable(true) + .enumerable(false) + .configurable(true), + ); + self + } + + /// Adds a new static data property to the builtin object. + fn static_property(mut self, key: K, value: V, attribute: Attribute) -> Self + where + K: Into, + V: Into, + { + let property = PropertyDescriptor::builder() + .value(value) + .writable(attribute.writable()) + .enumerable(attribute.enumerable()) + .configurable(attribute.configurable()); + self.object.insert(key, property); + self + } + + /// Specify the `[[Prototype]]` internal field of the builtin object. + /// + /// Default is `Function.prototype` for constructors and `Object.prototype` for statics. + fn prototype(mut self, prototype: JsObject) -> Self { + self.prototype = prototype; self } } diff --git a/boa_engine/src/builtins/number/globals.rs b/boa_engine/src/builtins/number/globals.rs index 3dd405c121..15ae25b6f4 100644 --- a/boa_engine/src/builtins/number/globals.rs +++ b/boa_engine/src/builtins/number/globals.rs @@ -38,8 +38,7 @@ pub(crate) struct IsFinite; impl IntrinsicObject for IsFinite { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(is_finite) + BuiltInBuilder::callable_with_intrinsic::(realm, is_finite) .name(Self::NAME) .length(1) .build(); @@ -85,8 +84,7 @@ pub(crate) struct IsNaN; impl IntrinsicObject for IsNaN { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(is_nan) + BuiltInBuilder::callable_with_intrinsic::(realm, is_nan) .name(Self::NAME) .length(1) .build(); @@ -227,8 +225,7 @@ pub(crate) struct ParseInt; impl IntrinsicObject for ParseInt { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(parse_int) + BuiltInBuilder::callable_with_intrinsic::(realm, parse_int) .name(Self::NAME) .length(2) .build(); @@ -301,8 +298,7 @@ pub(crate) struct ParseFloat; impl IntrinsicObject for ParseFloat { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(parse_float) + BuiltInBuilder::callable_with_intrinsic::(realm, parse_float) .name(Self::NAME) .length(1) .build(); diff --git a/boa_engine/src/builtins/number/mod.rs b/boa_engine/src/builtins/number/mod.rs index 2428ae8385..d87e2b2732 100644 --- a/boa_engine/src/builtins/number/mod.rs +++ b/boa_engine/src/builtins/number/mod.rs @@ -121,7 +121,11 @@ impl BuiltInConstructor for Number { } let prototype = get_prototype_from_constructor(new_target, StandardConstructors::number, context)?; - let this = JsObject::from_proto_and_data(prototype, ObjectData::number(data)); + let this = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::number(data), + ); Ok(this.into()) } } diff --git a/boa_engine/src/builtins/object/for_in_iterator.rs b/boa_engine/src/builtins/object/for_in_iterator.rs index a5665d7e5f..44af9f42f1 100644 --- a/boa_engine/src/builtins/object/for_in_iterator.rs +++ b/boa_engine/src/builtins/object/for_in_iterator.rs @@ -77,7 +77,8 @@ impl ForInIterator { /// /// [spec]: https://tc39.es/ecma262/#sec-createforiniterator pub(crate) fn create_for_in_iterator(object: JsValue, context: &Context<'_>) -> JsObject { - JsObject::from_proto_and_data( + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context .intrinsics() .objects() diff --git a/boa_engine/src/builtins/object/mod.rs b/boa_engine/src/builtins/object/mod.rs index fc97a94260..4c4fc5202a 100644 --- a/boa_engine/src/builtins/object/mod.rs +++ b/boa_engine/src/builtins/object/mod.rs @@ -48,13 +48,11 @@ impl IntrinsicObject for Object { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let legacy_proto_getter = BuiltInBuilder::new(realm) - .callable(Self::legacy_proto_getter) + let legacy_proto_getter = BuiltInBuilder::callable(realm, Self::legacy_proto_getter) .name("get __proto__") .build(); - let legacy_setter_proto = BuiltInBuilder::new(realm) - .callable(Self::legacy_proto_setter) + let legacy_setter_proto = BuiltInBuilder::callable(realm, Self::legacy_proto_setter) .name("set __proto__") .build(); @@ -142,7 +140,11 @@ impl BuiltInConstructor for Object { // a. Return ? OrdinaryCreateFromConstructor(NewTarget, "%Object.prototype%"). let prototype = get_prototype_from_constructor(new_target, StandardConstructors::object, context)?; - let object = JsObject::from_proto_and_data(prototype, ObjectData::ordinary()); + let object = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::ordinary(), + ); return Ok(object.into()); } @@ -419,7 +421,8 @@ impl Object { let properties = args.get_or_undefined(1); let obj = match prototype { - JsValue::Object(_) | JsValue::Null => JsObject::from_proto_and_data( + JsValue::Object(_) | JsValue::Null => JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype.as_object().cloned(), ObjectData::ordinary(), ), diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index b4b1ce2099..8e14f56b15 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -311,8 +311,7 @@ impl IntrinsicObject for Promise { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); @@ -384,7 +383,8 @@ impl BuiltInConstructor for Promise { let promise = get_prototype_from_constructor(new_target, StandardConstructors::promise, context)?; - let promise = JsObject::from_proto_and_data( + let promise = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), promise, // 4. Set promise.[[PromiseState]] to pending. // 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. diff --git a/boa_engine/src/builtins/proxy/mod.rs b/boa_engine/src/builtins/proxy/mod.rs index 37e678fba9..8e754c589a 100644 --- a/boa_engine/src/builtins/proxy/mod.rs +++ b/boa_engine/src/builtins/proxy/mod.rs @@ -36,9 +36,8 @@ impl IntrinsicObject for Proxy { let _timer = Profiler::global().start_event(Self::NAME, "init"); BuiltInBuilder::from_standard_constructor::(realm) - .no_proto() .static_method(Self::revocable, "revocable", 2) - .build(); + .build_without_prototype(); } fn get(intrinsics: &Intrinsics) -> JsObject { @@ -126,7 +125,8 @@ impl Proxy { // i. Set P.[[Construct]] as specified in 10.5.13. // 6. Set P.[[ProxyTarget]] to target. // 7. Set P.[[ProxyHandler]] to handler. - let p = JsObject::from_proto_and_data( + let p = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().constructors().object().prototype(), ObjectData::proxy( Self::new(target.clone(), handler.clone()), diff --git a/boa_engine/src/builtins/regexp/mod.rs b/boa_engine/src/builtins/regexp/mod.rs index 1edc2b1b28..7aa28d46bd 100644 --- a/boa_engine/src/builtins/regexp/mod.rs +++ b/boa_engine/src/builtins/regexp/mod.rs @@ -51,47 +51,37 @@ impl IntrinsicObject for RegExp { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; - let get_has_indices = BuiltInBuilder::new(realm) - .callable(Self::get_has_indices) + let get_has_indices = BuiltInBuilder::callable(realm, Self::get_has_indices) .name("get hasIndices") .build(); - let get_global = BuiltInBuilder::new(realm) - .callable(Self::get_global) + let get_global = BuiltInBuilder::callable(realm, Self::get_global) .name("get global") .build(); - let get_ignore_case = BuiltInBuilder::new(realm) - .callable(Self::get_ignore_case) + let get_ignore_case = BuiltInBuilder::callable(realm, Self::get_ignore_case) .name("get ignoreCase") .build(); - let get_multiline = BuiltInBuilder::new(realm) - .callable(Self::get_multiline) + let get_multiline = BuiltInBuilder::callable(realm, Self::get_multiline) .name("get multiline") .build(); - let get_dot_all = BuiltInBuilder::new(realm) - .callable(Self::get_dot_all) + let get_dot_all = BuiltInBuilder::callable(realm, Self::get_dot_all) .name("get dotAll") .build(); - let get_unicode = BuiltInBuilder::new(realm) - .callable(Self::get_unicode) + let get_unicode = BuiltInBuilder::callable(realm, Self::get_unicode) .name("get unicode") .build(); - let get_sticky = BuiltInBuilder::new(realm) - .callable(Self::get_sticky) + let get_sticky = BuiltInBuilder::callable(realm, Self::get_sticky) .name("get sticky") .build(); - let get_flags = BuiltInBuilder::new(realm) - .callable(Self::get_flags) + let get_flags = BuiltInBuilder::callable(realm, Self::get_flags) .name("get flags") .build(); - let get_source = BuiltInBuilder::new(realm) - .callable(Self::get_source) + let get_source = BuiltInBuilder::callable(realm, Self::get_source) .name("get source") .build(); let regexp = BuiltInBuilder::from_standard_constructor::(realm) @@ -238,7 +228,11 @@ impl RegExp { // 1. Let obj be ? OrdinaryCreateFromConstructor(newTarget, "%RegExp.prototype%", « [[RegExpMatcher]], [[OriginalSource]], [[OriginalFlags]] »). let proto = get_prototype_from_constructor(new_target, StandardConstructors::regexp, context)?; - let obj = JsObject::from_proto_and_data(proto, ObjectData::ordinary()); + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + proto, + ObjectData::ordinary(), + ); // 2. Perform ! DefinePropertyOrThrow(obj, "lastIndex", PropertyDescriptor { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }). obj.define_property_or_throw( diff --git a/boa_engine/src/builtins/regexp/regexp_string_iterator.rs b/boa_engine/src/builtins/regexp/regexp_string_iterator.rs index f17bac9ee0..0a16d673cf 100644 --- a/boa_engine/src/builtins/regexp/regexp_string_iterator.rs +++ b/boa_engine/src/builtins/regexp/regexp_string_iterator.rs @@ -103,7 +103,8 @@ impl RegExpStringIterator { // 5. Return ! CreateIteratorFromClosure(closure, "%RegExpStringIteratorPrototype%", %RegExpStringIteratorPrototype%). - let regexp_string_iterator = JsObject::from_proto_and_data( + let regexp_string_iterator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context .intrinsics() .objects() diff --git a/boa_engine/src/builtins/set/mod.rs b/boa_engine/src/builtins/set/mod.rs index a5e09a96bb..656f27c89e 100644 --- a/boa_engine/src/builtins/set/mod.rs +++ b/boa_engine/src/builtins/set/mod.rs @@ -44,18 +44,15 @@ impl IntrinsicObject for Set { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); - let size_getter = BuiltInBuilder::new(realm) - .callable(Self::size_getter) + let size_getter = BuiltInBuilder::callable(realm, Self::size_getter) .name("get size") .build(); - let values_function = BuiltInBuilder::new(realm) - .callable(Self::values) + let values_function = BuiltInBuilder::callable(realm, Self::values) .name("values") .build(); @@ -127,7 +124,11 @@ impl BuiltInConstructor for Set { // 3. Set set.[[SetData]] to a new empty List. let prototype = get_prototype_from_constructor(new_target, StandardConstructors::set, context)?; - let set = JsObject::from_proto_and_data(prototype, ObjectData::set(OrderedSet::default())); + let set = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::set(OrderedSet::default()), + ); // 4. If iterable is either undefined or null, return set. let iterable = args.get_or_undefined(0); @@ -173,7 +174,11 @@ impl Set { let prototype = prototype.unwrap_or_else(|| context.intrinsics().constructors().set().prototype()); - JsObject::from_proto_and_data(prototype, ObjectData::set(OrderedSet::new())) + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::set(OrderedSet::new()), + ) } /// Utility for constructing `Set` objects from an iterator of `JsValue`'s. diff --git a/boa_engine/src/builtins/set/set_iterator.rs b/boa_engine/src/builtins/set/set_iterator.rs index 56c60173a1..f49eab2a46 100644 --- a/boa_engine/src/builtins/set/set_iterator.rs +++ b/boa_engine/src/builtins/set/set_iterator.rs @@ -87,7 +87,8 @@ impl SetIterator { lock: SetLock, context: &Context<'_>, ) -> JsValue { - let set_iterator = JsObject::from_proto_and_data( + let set_iterator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().objects().iterator_prototypes().set(), ObjectData::set_iterator(Self::new(set, kind, lock)), ); diff --git a/boa_engine/src/builtins/string/mod.rs b/boa_engine/src/builtins/string/mod.rs index eb71a7d228..274b5346ce 100644 --- a/boa_engine/src/builtins/string/mod.rs +++ b/boa_engine/src/builtins/string/mod.rs @@ -80,14 +80,12 @@ impl IntrinsicObject for String { let symbol_iterator = JsSymbol::iterator(); - let trim_start = BuiltInBuilder::new(realm) - .callable(Self::trim_start) + let trim_start = BuiltInBuilder::callable(realm, Self::trim_start) .length(0) .name("trimStart") .build(); - let trim_end = BuiltInBuilder::new(realm) - .callable(Self::trim_end) + let trim_end = BuiltInBuilder::callable(realm, Self::trim_end) .length(0) .name("trimEnd") .build(); @@ -255,7 +253,11 @@ impl String { // 4. Set S.[[GetOwnProperty]] as specified in 10.4.3.1. // 5. Set S.[[DefineOwnProperty]] as specified in 10.4.3.2. // 6. Set S.[[OwnPropertyKeys]] as specified in 10.4.3.3. - let s = JsObject::from_proto_and_data(prototype, ObjectData::string(value)); + let s = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::string(value), + ); // 8. Perform ! DefinePropertyOrThrow(S, "length", PropertyDescriptor { [[Value]]: 𝔽(length), // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }). diff --git a/boa_engine/src/builtins/string/string_iterator.rs b/boa_engine/src/builtins/string/string_iterator.rs index f39aabd7df..52a8083a33 100644 --- a/boa_engine/src/builtins/string/string_iterator.rs +++ b/boa_engine/src/builtins/string/string_iterator.rs @@ -60,7 +60,8 @@ impl IntrinsicObject for StringIterator { impl StringIterator { /// Create a new `StringIterator`. pub fn create_string_iterator(string: JsString, context: &mut Context<'_>) -> JsObject { - JsObject::from_proto_and_data( + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context .intrinsics() .objects() diff --git a/boa_engine/src/builtins/symbol/mod.rs b/boa_engine/src/builtins/symbol/mod.rs index 4747373e92..64a5a56632 100644 --- a/boa_engine/src/builtins/symbol/mod.rs +++ b/boa_engine/src/builtins/symbol/mod.rs @@ -111,14 +111,12 @@ impl IntrinsicObject for Symbol { let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - let to_primitive = BuiltInBuilder::new(realm) - .callable(Self::to_primitive) + let to_primitive = BuiltInBuilder::callable(realm, Self::to_primitive) .name("[Symbol.toPrimitive]") .length(1) .build(); - let get_description = BuiltInBuilder::new(realm) - .callable(Self::get_description) + let get_description = BuiltInBuilder::callable(realm, Self::get_description) .name("get description") .build(); diff --git a/boa_engine/src/builtins/typed_array/mod.rs b/boa_engine/src/builtins/typed_array/mod.rs index 4c3570dfc1..e80ec6054b 100644 --- a/boa_engine/src/builtins/typed_array/mod.rs +++ b/boa_engine/src/builtins/typed_array/mod.rs @@ -51,8 +51,7 @@ macro_rules! typed_array { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(realm) - .callable(TypedArray::get_species) + let get_species = BuiltInBuilder::callable(realm, TypedArray::get_species) .name("get [Symbol.species]") .build(); @@ -241,38 +240,31 @@ pub(crate) struct TypedArray; impl IntrinsicObject for TypedArray { fn init(realm: &Realm) { - let get_species = BuiltInBuilder::new(realm) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(realm, Self::get_species) .name("get [Symbol.species]") .build(); - let get_buffer = BuiltInBuilder::new(realm) - .callable(Self::buffer) + let get_buffer = BuiltInBuilder::callable(realm, Self::buffer) .name("get buffer") .build(); - let get_byte_length = BuiltInBuilder::new(realm) - .callable(Self::byte_length) + let get_byte_length = BuiltInBuilder::callable(realm, Self::byte_length) .name("get byteLength") .build(); - let get_byte_offset = BuiltInBuilder::new(realm) - .callable(Self::byte_offset) + let get_byte_offset = BuiltInBuilder::callable(realm, Self::byte_offset) .name("get byteOffset") .build(); - let get_length = BuiltInBuilder::new(realm) - .callable(Self::length) + let get_length = BuiltInBuilder::callable(realm, Self::length) .name("get length") .build(); - let get_to_string_tag = BuiltInBuilder::new(realm) - .callable(Self::to_string_tag) + let get_to_string_tag = BuiltInBuilder::callable(realm, Self::to_string_tag) .name("get [Symbol.toStringTag]") .build(); - let values_function = BuiltInBuilder::new(realm) - .callable(Self::values) + let values_function = BuiltInBuilder::callable(realm, Self::values) .name("values") .length(0) .build(); @@ -284,11 +276,6 @@ impl IntrinsicObject for TypedArray { None, Attribute::CONFIGURABLE, ) - .property( - utf16!("length"), - 0, - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, - ) .property( JsSymbol::iterator(), values_function, @@ -3205,7 +3192,11 @@ impl TypedArray { } // 2. Let obj be ! IntegerIndexedObjectCreate(proto). - let obj = JsObject::from_proto_and_data(proto, ObjectData::integer_indexed(indexed)); + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + proto, + ObjectData::integer_indexed(indexed), + ); // 9. Return obj. Ok(obj) diff --git a/boa_engine/src/builtins/uri/mod.rs b/boa_engine/src/builtins/uri/mod.rs index 60585f05aa..20ff40701e 100644 --- a/boa_engine/src/builtins/uri/mod.rs +++ b/boa_engine/src/builtins/uri/mod.rs @@ -82,8 +82,7 @@ pub(crate) struct DecodeUri; impl IntrinsicObject for DecodeUri { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(decode_uri) + BuiltInBuilder::callable_with_intrinsic::(realm, decode_uri) .name(Self::NAME) .length(1) .build(); @@ -101,8 +100,7 @@ pub(crate) struct DecodeUriComponent; impl IntrinsicObject for DecodeUriComponent { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(decode_uri_component) + BuiltInBuilder::callable_with_intrinsic::(realm, decode_uri_component) .name(Self::NAME) .length(1) .build(); @@ -124,8 +122,7 @@ pub(crate) struct EncodeUri; impl IntrinsicObject for EncodeUri { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(encode_uri) + BuiltInBuilder::callable_with_intrinsic::(realm, encode_uri) .name(Self::NAME) .length(1) .build(); @@ -142,8 +139,7 @@ pub(crate) struct EncodeUriComponent; impl IntrinsicObject for EncodeUriComponent { fn init(realm: &Realm) { - BuiltInBuilder::with_intrinsic::(realm) - .callable(encode_uri_component) + BuiltInBuilder::callable_with_intrinsic::(realm, encode_uri_component) .name(Self::NAME) .length(1) .build(); diff --git a/boa_engine/src/builtins/weak/weak_ref.rs b/boa_engine/src/builtins/weak/weak_ref.rs index 56f9e4cc5d..a12ef32d32 100644 --- a/boa_engine/src/builtins/weak/weak_ref.rs +++ b/boa_engine/src/builtins/weak/weak_ref.rs @@ -80,7 +80,8 @@ impl BuiltInConstructor for WeakRef { // 3. Let weakRef be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakRef.prototype%", « [[WeakRefTarget]] »). // 5. Set weakRef.[[WeakRefTarget]] to target. - let weak_ref = JsObject::from_proto_and_data( + let weak_ref = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), get_prototype_from_constructor(new_target, StandardConstructors::weak_ref, context)?, ObjectData::weak_ref(WeakGc::new(target.inner())), ); @@ -160,7 +161,10 @@ mod tests { "#}, |v, _| v.is_object(), ), - TestAction::inspect_context(|_| boa_gc::force_collect()), + TestAction::inspect_context(|context| { + context.clear_kept_objects(); + boa_gc::force_collect(); + }), TestAction::assert_eq("ptr.deref()", JsValue::undefined()), ]); } diff --git a/boa_engine/src/builtins/weak_map/mod.rs b/boa_engine/src/builtins/weak_map/mod.rs index 07155e4481..788cef1ea3 100644 --- a/boa_engine/src/builtins/weak_map/mod.rs +++ b/boa_engine/src/builtins/weak_map/mod.rs @@ -82,7 +82,8 @@ impl BuiltInConstructor for WeakMap { // 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakMap.prototype%", « [[WeakMapData]] »). // 3. Set map.[[WeakMapData]] to a new empty List. - let map = JsObject::from_proto_and_data( + let map = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), get_prototype_from_constructor(new_target, StandardConstructors::weak_map, context)?, ObjectData::weak_map(boa_gc::WeakMap::new()), ); diff --git a/boa_engine/src/builtins/weak_set/mod.rs b/boa_engine/src/builtins/weak_set/mod.rs index 2294ddd5ec..68f2bb4aee 100644 --- a/boa_engine/src/builtins/weak_set/mod.rs +++ b/boa_engine/src/builtins/weak_set/mod.rs @@ -78,7 +78,8 @@ impl BuiltInConstructor for WeakSet { // 2. Let set be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakSet.prototype%", « [[WeakSetData]] »). // 3. Set set.[[WeakSetData]] to a new empty List. - let weak_set = JsObject::from_proto_and_data( + let weak_set = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), get_prototype_from_constructor(new_target, StandardConstructors::weak_set, context)?, ObjectData::weak_set(WeakMap::new()), ); diff --git a/boa_engine/src/class.rs b/boa_engine/src/class.rs index 2d3f64d196..1c4bb6ad96 100644 --- a/boa_engine/src/class.rs +++ b/boa_engine/src/class.rs @@ -151,8 +151,11 @@ impl ClassConstructor for T { .unwrap_or_else(|| class_prototype.clone()); let native_instance = Self::constructor(this, args, context)?; - let object_instance = - JsObject::from_proto_and_data(prototype, ObjectData::native_object(native_instance)); + let object_instance = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::native_object(native_instance), + ); Ok(object_instance.into()) } } diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 50a684ea98..879ce8dd01 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -4,22 +4,40 @@ use boa_gc::{Finalize, Trace}; use crate::{ builtins::{iterable::IteratorPrototypes, uri::UriFunctions}, - object::{JsFunction, JsObject, ObjectData}, + object::{ + shape::shared_shape::{template::ObjectTemplate, SharedShape}, + JsFunction, JsObject, ObjectData, CONSTRUCTOR, PROTOTYPE, + }, + property::{Attribute, PropertyKey}, + JsSymbol, }; /// The intrinsic objects and constructors. /// /// `Intrinsics` is internally stored using a `Gc`, which makes it cheapily clonable /// for multiple references to the same set of intrinsic objects. -#[derive(Debug, Default, Trace, Finalize)] +#[derive(Debug, Trace, Finalize)] pub struct Intrinsics { /// Cached standard constructors pub(super) constructors: StandardConstructors, /// Cached intrinsic objects pub(super) objects: IntrinsicObjects, + /// Cached object templates. + pub(super) templates: ObjectTemplates, } impl Intrinsics { + pub(crate) fn new(root_shape: &SharedShape) -> Self { + let constructors = StandardConstructors::default(); + let templates = ObjectTemplates::new(root_shape, &constructors); + + Self { + constructors, + objects: IntrinsicObjects::default(), + templates, + } + } + /// Return the cached intrinsic objects. #[inline] pub const fn objects(&self) -> &IntrinsicObjects { @@ -31,6 +49,10 @@ impl Intrinsics { pub const fn constructors(&self) -> &StandardConstructors { &self.constructors } + + pub(crate) const fn templates(&self) -> &ObjectTemplates { + &self.templates + } } /// Store a builtin constructor (such as `Object`) and its corresponding prototype. @@ -950,3 +972,317 @@ impl IntrinsicObjects { self.segments_prototype.clone() } } + +/// Contains commonly used [`ObjectTemplate`]s. +#[derive(Debug, Trace, Finalize)] +pub(crate) struct ObjectTemplates { + iterator_result: ObjectTemplate, + ordinary_object: ObjectTemplate, + array: ObjectTemplate, + number: ObjectTemplate, + string: ObjectTemplate, + symbol: ObjectTemplate, + bigint: ObjectTemplate, + boolean: ObjectTemplate, + + unmapped_arguments: ObjectTemplate, + mapped_arguments: ObjectTemplate, + + function_with_prototype: ObjectTemplate, + function_prototype: ObjectTemplate, + + function: ObjectTemplate, + async_function: ObjectTemplate, + + function_without_proto: ObjectTemplate, + function_with_prototype_without_proto: ObjectTemplate, +} + +impl ObjectTemplates { + pub(crate) fn new(root_shape: &SharedShape, constructors: &StandardConstructors) -> Self { + // pre-initialize used shapes. + let ordinary_object = + ObjectTemplate::with_prototype(root_shape, constructors.object().prototype()); + let mut array = ObjectTemplate::new(root_shape); + let length_property_key: PropertyKey = "length".into(); + array.property( + length_property_key.clone(), + Attribute::WRITABLE | Attribute::PERMANENT | Attribute::NON_ENUMERABLE, + ); + array.set_prototype(constructors.array().prototype()); + + let number = ObjectTemplate::with_prototype(root_shape, constructors.number().prototype()); + let symbol = ObjectTemplate::with_prototype(root_shape, constructors.symbol().prototype()); + let bigint = ObjectTemplate::with_prototype(root_shape, constructors.bigint().prototype()); + let boolean = + ObjectTemplate::with_prototype(root_shape, constructors.boolean().prototype()); + let mut string = ObjectTemplate::new(root_shape); + string.property( + length_property_key.clone(), + Attribute::READONLY | Attribute::PERMANENT | Attribute::NON_ENUMERABLE, + ); + string.set_prototype(constructors.string().prototype()); + + let name_property_key: PropertyKey = "name".into(); + let mut function = ObjectTemplate::new(root_shape); + function.property( + length_property_key.clone(), + Attribute::READONLY | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ); + function.property( + name_property_key, + Attribute::READONLY | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ); + + let function_without_proto = function.clone(); + let mut async_function = function.clone(); + let mut function_with_prototype = function.clone(); + + function_with_prototype.property( + PROTOTYPE.into(), + Attribute::WRITABLE | Attribute::PERMANENT | Attribute::NON_ENUMERABLE, + ); + + let function_with_prototype_without_proto = function_with_prototype.clone(); + + function.set_prototype(constructors.function().prototype()); + function_with_prototype.set_prototype(constructors.function().prototype()); + async_function.set_prototype(constructors.async_function().prototype()); + + let mut function_prototype = ordinary_object.clone(); + function_prototype.property( + CONSTRUCTOR.into(), + Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ); + + let mut unmapped_arguments = ordinary_object.clone(); + + // 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + unmapped_arguments.property( + length_property_key, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); + + // 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + unmapped_arguments.property( + JsSymbol::iterator().into(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); + + let mut mapped_arguments = unmapped_arguments.clone(); + + // 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, + // [[Configurable]]: false }). + unmapped_arguments.accessor( + "callee".into(), + true, + true, + Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ); + + // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + mapped_arguments.property( + "callee".into(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); + + let mut iterator_result = ordinary_object.clone(); + iterator_result.property( + "value".into(), + Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::ENUMERABLE, + ); + iterator_result.property( + "done".into(), + Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::ENUMERABLE, + ); + + Self { + iterator_result, + ordinary_object, + array, + number, + string, + symbol, + bigint, + boolean, + unmapped_arguments, + mapped_arguments, + function_with_prototype, + function_prototype, + function, + async_function, + function_without_proto, + function_with_prototype_without_proto, + } + } + + /// Cached iterator result template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Object.prototype` + /// 2. `"done"`: (`WRITABLE`, `CONFIGURABLE`, `ENUMERABLE`) + /// 3. `"value"`: (`WRITABLE`, `CONFIGURABLE`, `ENUMERABLE`) + pub(crate) const fn iterator_result(&self) -> &ObjectTemplate { + &self.iterator_result + } + + /// Cached ordinary object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Object.prototype` + pub(crate) const fn ordinary_object(&self) -> &ObjectTemplate { + &self.ordinary_object + } + + /// Cached array object template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`WRITABLE`, `PERMANENT`,`NON_ENUMERABLE`) + /// 2. `__proto__`: `Array.prototype` + pub(crate) const fn array(&self) -> &ObjectTemplate { + &self.array + } + + /// Cached number object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Number.prototype` + pub(crate) const fn number(&self) -> &ObjectTemplate { + &self.number + } + + /// Cached string object template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`READONLY`, `PERMANENT`,`NON_ENUMERABLE`) + /// 2. `__proto__`: `String.prototype` + pub(crate) const fn string(&self) -> &ObjectTemplate { + &self.string + } + + /// Cached symbol object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Symbol.prototype` + pub(crate) const fn symbol(&self) -> &ObjectTemplate { + &self.symbol + } + + /// Cached bigint object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `BigInt.prototype` + pub(crate) const fn bigint(&self) -> &ObjectTemplate { + &self.bigint + } + + /// Cached boolean object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Boolean.prototype` + pub(crate) const fn boolean(&self) -> &ObjectTemplate { + &self.boolean + } + + /// Cached unmapped arguments object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Object.prototype` + /// 2. `"length"`: (`WRITABLE`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 3. `@@iterator`: (`WRITABLE`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 4. `get/set` `"callee"`: (`NON_ENUMERABLE`, `PERMANENT`) + pub(crate) const fn unmapped_arguments(&self) -> &ObjectTemplate { + &self.unmapped_arguments + } + + /// Cached mapped arguments object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Object.prototype` + /// 2. `"length"`: (`WRITABLE`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 3. `@@iterator`: (`WRITABLE`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 4. `"callee"`: (`WRITABLE`, `NON_ENUMERABLE`, `CONFIGURABLE`) + pub(crate) const fn mapped_arguments(&self) -> &ObjectTemplate { + &self.mapped_arguments + } + + /// Cached function object with `"prototype"` property template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 2. `"name"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 3. `"prototype"`: (`WRITABLE`, `PERMANENT`, `NON_ENUMERABLE`) + /// 4. `__proto__`: `Function.prototype` + pub(crate) const fn function_with_prototype(&self) -> &ObjectTemplate { + &self.function_with_prototype + } + + /// Cached constructor function object template. + /// + /// Transitions: + /// + /// 1. `__proto__`: `Object.prototype` + /// 2. `"contructor"`: (`WRITABLE`, `CONFIGURABLE`, `NON_ENUMERABLE`) + pub(crate) const fn function_prototype(&self) -> &ObjectTemplate { + &self.function_prototype + } + + /// Cached function object property template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 2. `"name"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 3. `__proto__`: `Function.prototype` + pub(crate) const fn function(&self) -> &ObjectTemplate { + &self.function + } + + /// Cached function object property template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 2. `"name"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 3. `__proto__`: `AsyncFunction.prototype` + pub(crate) const fn async_function(&self) -> &ObjectTemplate { + &self.async_function + } + + /// Cached function object without `__proto__` template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 2. `"name"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + pub(crate) const fn function_without_proto(&self) -> &ObjectTemplate { + &self.function_without_proto + } + + /// Cached function object with `"prototype"` and without `__proto__` template. + /// + /// Transitions: + /// + /// 1. `"length"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 2. `"name"`: (`READONLY`, `NON_ENUMERABLE`, `CONFIGURABLE`) + /// 3. `"prototype"`: (`WRITABLE`, `PERMANENT`, `NON_ENUMERABLE`) + pub(crate) const fn function_with_prototype_without_proto(&self) -> &ObjectTemplate { + &self.function_with_prototype_without_proto + } +} diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index a78c1b1dd8..e8d6e0d600 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -22,7 +22,7 @@ use crate::{ class::{Class, ClassBuilder}, job::{JobQueue, NativeJob, SimpleJobQueue}, native_function::NativeFunction, - object::{FunctionObjectBuilder, JsObject}, + object::{shape::SharedShape, FunctionObjectBuilder, JsObject}, optimizer::{Optimizer, OptimizerOptions, OptimizerStatistics}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, @@ -104,6 +104,7 @@ pub struct Context<'host> { job_queue: MaybeShared<'host, dyn JobQueue>, optimizer_options: OptimizerOptions, + root_shape: SharedShape, } impl std::fmt::Debug for Context<'_> { @@ -536,6 +537,10 @@ impl<'host> Context<'host> { std::mem::replace(&mut self.realm, realm) } + pub(crate) fn root_shape(&self) -> SharedShape { + self.root_shape.clone() + } + /// Gets the host hooks. pub fn host_hooks(&self) -> MaybeShared<'host, dyn HostHooks> { self.host_hooks.clone() @@ -700,11 +705,13 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { 'hooks: 'host, 'queue: 'host, { + let root_shape = SharedShape::root(); + let host_hooks = self.host_hooks.unwrap_or_else(|| { let hooks: &dyn HostHooks = &DefaultHooks; hooks.into() }); - let realm = Realm::create(&*host_hooks); + let realm = Realm::create(&*host_hooks, &root_shape); let vm = Vm::new(realm.environment().clone()); let mut context = Context { @@ -727,6 +734,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { queue.into() }), optimizer_options: OptimizerOptions::OPTIMIZE_ALL, + root_shape, }; builtins::set_default_global_bindings(&mut context)?; diff --git a/boa_engine/src/error.rs b/boa_engine/src/error.rs index e57aa4fc5c..280449d076 100644 --- a/boa_engine/src/error.rs +++ b/boa_engine/src/error.rs @@ -709,7 +709,11 @@ impl JsNativeError { } }; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(tag)); + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::error(tag), + ); o.create_non_enumerable_data_property_or_throw(utf16!("message"), &**message, context); diff --git a/boa_engine/src/object/builtins/jsarraybuffer.rs b/boa_engine/src/object/builtins/jsarraybuffer.rs index 9a94dcca74..324d5d60c7 100644 --- a/boa_engine/src/object/builtins/jsarraybuffer.rs +++ b/boa_engine/src/object/builtins/jsarraybuffer.rs @@ -101,7 +101,8 @@ impl JsArrayBuffer { // 3. Set obj.[[ArrayBufferData]] to block. // 4. Set obj.[[ArrayBufferByteLength]] to byteLength. - let obj = JsObject::from_proto_and_data( + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::array_buffer(ArrayBuffer { array_buffer_data: Some(block), diff --git a/boa_engine/src/object/builtins/jsdataview.rs b/boa_engine/src/object/builtins/jsdataview.rs index 5284db3d1d..960e007358 100644 --- a/boa_engine/src/object/builtins/jsdataview.rs +++ b/boa_engine/src/object/builtins/jsdataview.rs @@ -97,7 +97,8 @@ impl JsDataView { let prototype = get_prototype_from_constructor(&constructor, StandardConstructors::data_view, context)?; - let obj = JsObject::from_proto_and_data( + let obj = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::data_view(DataView { viewed_array_buffer: (**array_buffer).clone(), diff --git a/boa_engine/src/object/builtins/jsdate.rs b/boa_engine/src/object/builtins/jsdate.rs index e6551a0750..ab23537aa7 100644 --- a/boa_engine/src/object/builtins/jsdate.rs +++ b/boa_engine/src/object/builtins/jsdate.rs @@ -43,7 +43,8 @@ impl JsDate { #[inline] pub fn new(context: &mut Context<'_>) -> Self { let prototype = context.intrinsics().constructors().date().prototype(); - let inner = JsObject::from_proto_and_data( + let inner = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::date(Date::utc_now(&*context.host_hooks())), ); @@ -574,7 +575,11 @@ impl JsDate { let date_time = Date::new(Some(date_time.naive_local().timestamp_millis())); Ok(Self { - inner: JsObject::from_proto_and_data(prototype, ObjectData::date(date_time)), + inner: JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::date(date_time), + ), }) } } diff --git a/boa_engine/src/object/builtins/jsmap.rs b/boa_engine/src/object/builtins/jsmap.rs index 34f213a0f5..98705788e6 100644 --- a/boa_engine/src/object/builtins/jsmap.rs +++ b/boa_engine/src/object/builtins/jsmap.rs @@ -185,7 +185,11 @@ impl JsMap { let prototype = context.intrinsics().constructors().map().prototype(); // Create a default map object with [[MapData]] as a new empty list - JsObject::from_proto_and_data(prototype, ObjectData::map(OrderedMap::new())) + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::map(OrderedMap::new()), + ) } /// Returns a new [`JsMapIterator`] object that yields the `[key, value]` pairs within the [`JsMap`] in insertion order. diff --git a/boa_engine/src/object/builtins/jspromise.rs b/boa_engine/src/object/builtins/jspromise.rs index 13d8439b13..6159b75571 100644 --- a/boa_engine/src/object/builtins/jspromise.rs +++ b/boa_engine/src/object/builtins/jspromise.rs @@ -154,7 +154,8 @@ impl JsPromise { where F: FnOnce(&ResolvingFunctions, &mut Context<'_>) -> JsResult, { - let promise = JsObject::from_proto_and_data( + let promise = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().constructors().promise().prototype(), ObjectData::promise(Promise::new()), ); @@ -201,7 +202,8 @@ impl JsPromise { /// ``` #[inline] pub fn new_pending(context: &mut Context<'_>) -> (JsPromise, ResolvingFunctions) { - let promise = JsObject::from_proto_and_data( + let promise = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().constructors().promise().prototype(), ObjectData::promise(Promise::new()), ); diff --git a/boa_engine/src/object/builtins/jsproxy.rs b/boa_engine/src/object/builtins/jsproxy.rs index 3e9491e346..575263562c 100644 --- a/boa_engine/src/object/builtins/jsproxy.rs +++ b/boa_engine/src/object/builtins/jsproxy.rs @@ -518,7 +518,8 @@ impl JsProxyBuilder { let callable = self.target.is_callable(); let constructor = self.target.is_constructor(); - let proxy = JsObject::from_proto_and_data( + let proxy = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().constructors().object().prototype(), ObjectData::proxy(Proxy::new(self.target, handler), callable, constructor), ); diff --git a/boa_engine/src/object/internal_methods/integer_indexed.rs b/boa_engine/src/object/internal_methods/integer_indexed.rs index 7e4668fe02..e5fb7909db 100644 --- a/boa_engine/src/object/internal_methods/integer_indexed.rs +++ b/boa_engine/src/object/internal_methods/integer_indexed.rs @@ -273,30 +273,19 @@ pub(crate) fn integer_indexed_exotic_own_property_keys( vec![] } else { // 2. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is false, then - // a. For each integer i starting with 0 such that i < O.[[ArrayLength]], in ascending order, do - // i. Add ! ToString(𝔽(i)) as the last element of keys. + // a. For each integer i starting with 0 such that i < O.[[ArrayLength]], in ascending order, do + // i. Add ! ToString(𝔽(i)) as the last element of keys. (0..inner.array_length()) .map(|index| PropertyKey::Index(index as u32)) .collect() }; // 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .string_property_keys() - .cloned() - .map(Into::into), - ); - + // a. Add P as the last element of keys. + // // 4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(obj.properties.shape.keys()); // 5. Return keys. Ok(keys) diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index 9079630a00..2660b4c444 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -340,14 +340,12 @@ pub(crate) fn ordinary_set_prototype_of( _: &mut Context<'_>, ) -> JsResult { // 1. Assert: Either Type(V) is Object or Type(V) is Null. - { - // 2. Let current be O.[[Prototype]]. - let current = obj.prototype(); + // 2. Let current be O.[[Prototype]]. + let current = obj.prototype(); - // 3. If SameValue(V, current) is true, return true. - if val == *current { - return Ok(true); - } + // 3. If SameValue(V, current) is true, return true. + if val == current { + return Ok(true); } // 4. Let extensible be O.[[Extensible]]. @@ -375,7 +373,7 @@ pub(crate) fn ordinary_set_prototype_of( break; } // ii. Else, set p to p.[[Prototype]]. - p = proto.prototype().clone(); + p = proto.prototype(); } // 9. Set O.[[Prototype]] to V. @@ -705,24 +703,11 @@ pub(crate) fn ordinary_own_property_keys( keys.extend(ordered_indexes.into_iter().map(Into::into)); // 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.borrow() - .properties - .string_property_keys() - .cloned() - .map(Into::into), - ); - + // a. Add P as the last element of keys. + // // 4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.borrow() - .properties - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(obj.borrow().properties.shape.keys()); // 5. Return keys. Ok(keys) diff --git a/boa_engine/src/object/internal_methods/string.rs b/boa_engine/src/object/internal_methods/string.rs index c415174169..0c919b5fab 100644 --- a/boa_engine/src/object/internal_methods/string.rs +++ b/boa_engine/src/object/internal_methods/string.rs @@ -101,12 +101,12 @@ pub(crate) fn string_exotic_own_property_keys( let mut keys = Vec::with_capacity(len); // 5. For each integer i starting with 0 such that i < len, in ascending order, do - // a. Add ! ToString(𝔽(i)) as the last element of keys. + // a. Add ! ToString(𝔽(i)) as the last element of keys. keys.extend((0..len).map(Into::into)); // 6. For each own property key P of O such that P is an array index // and ! ToIntegerOrInfinity(P) ≥ len, in ascending numeric index order, do - // a. Add P as the last element of keys. + // a. Add P as the last element of keys. let mut remaining_indices: Vec<_> = obj .properties .index_property_keys() @@ -117,23 +117,12 @@ pub(crate) fn string_exotic_own_property_keys( // 7. For each own property key P of O such that Type(P) is String and P is not // an array index, in ascending chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .string_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. // 8. For each own property key P of O such that Type(P) is Symbol, in ascending // chronological order of property creation, do - // a. Add P as the last element of keys. - keys.extend( - obj.properties - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(obj.properties.shape.keys()); // 9. Return keys. Ok(keys) diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index 9e7d3d8bfc..9ad7cd52f6 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -4,6 +4,7 @@ use super::{ internal_methods::{InternalObjectMethods, ARRAY_EXOTIC_INTERNAL_METHODS}, + shape::{shared_shape::SharedShape, Shape}, JsPrototype, NativeObject, Object, PropertyMap, }; use crate::{ @@ -21,6 +22,7 @@ use std::{ collections::HashMap, error::Error, fmt::{self, Debug, Display}, + hash::Hash, result::Result as StdResult, }; use thin_vec::ThinVec; @@ -108,8 +110,35 @@ impl JsObject { inner: Gc::new(VTableObject { object: GcRefCell::new(Object { kind: data.kind, - properties: PropertyMap::default(), - prototype: prototype.into(), + properties: PropertyMap::from_prototype_unique_shape(prototype.into()), + extensible: true, + private_elements: ThinVec::new(), + }), + vtable: data.internal_methods, + }), + } + } + + /// Creates a new object with the provided prototype and object data. + /// + /// This is equivalent to calling the specification's abstract operation [`OrdinaryObjectCreate`], + /// with the difference that the `additionalInternalSlotsList` parameter is automatically set by + /// the [`ObjectData`] provided. + /// + /// [`OrdinaryObjectCreate`]: https://tc39.es/ecma262/#sec-ordinaryobjectcreate + pub(crate) fn from_proto_and_data_with_shared_shape>>( + root_shape: SharedShape, + prototype: O, + data: ObjectData, + ) -> Self { + Self { + inner: Gc::new(VTableObject { + object: GcRefCell::new(Object { + kind: data.kind, + properties: PropertyMap::from_prototype_with_shared_shape( + Shape::shared(root_shape), + prototype.into(), + ), extensible: true, private_elements: ThinVec::new(), }), @@ -314,8 +343,8 @@ impl JsObject { /// Panics if the object is currently mutably borrowed. #[inline] #[track_caller] - pub fn prototype(&self) -> Ref<'_, JsPrototype> { - Ref::map(self.borrow(), Object::prototype) + pub fn prototype(&self) -> JsPrototype { + self.borrow().prototype() } /// Get the extensibility of the object. @@ -859,7 +888,7 @@ Cannot both specify accessors and a value or writable attribute", /// Helper function for property insertion. #[track_caller] - pub(crate) fn insert(&self, key: K, property: P) -> Option + pub(crate) fn insert(&self, key: K, property: P) -> bool where K: Into, P: Into, @@ -869,9 +898,9 @@ Cannot both specify accessors and a value or writable attribute", /// Inserts a field in the object `properties` without checking if it's writable. /// - /// If a field was already in the object with the same name that a `Some` is returned - /// with that field, otherwise None is returned. - pub fn insert_property(&self, key: K, property: P) -> Option + /// If a field was already in the object with the same name, than `true` is returned + /// with that field, otherwise `false` is returned. + pub fn insert_property(&self, key: K, property: P) -> bool where K: Into, P: Into, @@ -937,6 +966,14 @@ impl PartialEq for JsObject { } } +impl Eq for JsObject {} + +impl Hash for JsObject { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.as_ref(), state); + } +} + /// An error returned by [`JsObject::try_borrow`](struct.JsObject.html#method.try_borrow). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BorrowError; diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index b15f1a8dab..ed69064d16 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -8,20 +8,23 @@ pub use operations::IntegrityLevel; pub use property_map::*; use thin_vec::ThinVec; -use self::internal_methods::{ - arguments::ARGUMENTS_EXOTIC_INTERNAL_METHODS, - array::ARRAY_EXOTIC_INTERNAL_METHODS, - bound_function::{ - BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS, +use self::{ + internal_methods::{ + arguments::ARGUMENTS_EXOTIC_INTERNAL_METHODS, + array::ARRAY_EXOTIC_INTERNAL_METHODS, + bound_function::{ + BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS, + }, + function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, + integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS, + proxy::{ + PROXY_EXOTIC_INTERNAL_METHODS_ALL, PROXY_EXOTIC_INTERNAL_METHODS_BASIC, + PROXY_EXOTIC_INTERNAL_METHODS_WITH_CALL, + }, + string::STRING_EXOTIC_INTERNAL_METHODS, + InternalObjectMethods, ORDINARY_INTERNAL_METHODS, }, - function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, - integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS, - proxy::{ - PROXY_EXOTIC_INTERNAL_METHODS_ALL, PROXY_EXOTIC_INTERNAL_METHODS_BASIC, - PROXY_EXOTIC_INTERNAL_METHODS_WITH_CALL, - }, - string::STRING_EXOTIC_INTERNAL_METHODS, - InternalObjectMethods, ORDINARY_INTERNAL_METHODS, + shape::Shape, }; #[cfg(feature = "intl")] use crate::builtins::intl::{ @@ -74,6 +77,7 @@ pub mod builtins; mod jsobject; mod operations; mod property_map; +pub mod shape; pub(crate) use builtins::*; @@ -97,6 +101,11 @@ pub const PROTOTYPE: &[u16] = utf16!("prototype"); /// A `None` values means that the prototype is the `null` value. pub type JsPrototype = Option; +/// The internal storage of an object's property values. +/// +/// The [`shape::Shape`] contains the property names and attributes. +pub(crate) type ObjectStorage = Vec; + /// This trait allows Rust types to be passed around as objects. /// /// This is automatically implemented when a type implements `Any` and `Trace`. @@ -125,8 +134,6 @@ pub struct Object { kind: ObjectKind, /// The collection of properties contained in the object properties: PropertyMap, - /// Instance prototype `__proto__`. - prototype: JsPrototype, /// Whether it can have new properties added to it. pub(crate) extensible: bool, /// The `[[PrivateElements]]` internal slot. @@ -138,7 +145,6 @@ impl Default for Object { Self { kind: ObjectKind::Ordinary, properties: PropertyMap::default(), - prototype: None, extensible: true, private_elements: ThinVec::new(), } @@ -149,7 +155,6 @@ unsafe impl Trace for Object { boa_gc::custom_trace!(this, { mark(&this.kind); mark(&this.properties); - mark(&this.prototype); for (_, element) in &this.private_elements { mark(element); } @@ -838,6 +843,11 @@ impl Object { &mut self.kind } + /// Returns the shape of the object. + pub const fn shape(&self) -> &Shape { + &self.properties.shape + } + /// Returns the kind of the object. #[inline] pub const fn kind(&self) -> &ObjectKind { @@ -1478,8 +1488,8 @@ impl Object { /// Gets the prototype instance of this object. #[inline] - pub const fn prototype(&self) -> &JsPrototype { - &self.prototype + pub fn prototype(&self) -> JsPrototype { + self.properties.shape.prototype() } /// Sets the prototype instance of the object. @@ -1491,12 +1501,12 @@ impl Object { pub fn set_prototype>(&mut self, prototype: O) -> bool { let prototype = prototype.into(); if self.extensible { - self.prototype = prototype; + self.properties.shape = self.properties.shape.change_prototype_transition(prototype); true } else { // If target is non-extensible, [[SetPrototypeOf]] must return false // unless V is the SameValue as the target's observed [[GetPrototypeOf]] value. - self.prototype == prototype + self.prototype() == prototype } } @@ -1682,9 +1692,9 @@ impl Object { /// Inserts a field in the object `properties` without checking if it's writable. /// - /// If a field was already in the object with the same name, then a `Some` is returned - /// with that field's value, otherwise, `None` is returned. - pub(crate) fn insert(&mut self, key: K, property: P) -> Option + /// If a field was already in the object with the same name, then `true` is returned + /// otherwise, `false` is returned. + pub(crate) fn insert(&mut self, key: K, property: P) -> bool where K: Into, P: Into, @@ -1692,9 +1702,11 @@ impl Object { self.properties.insert(&key.into(), property.into()) } - /// Helper function for property removal. + /// Helper function for property removal without checking if it's configurable. + /// + /// Returns `true` if the property was removed, `false` otherwise. #[inline] - pub(crate) fn remove(&mut self, key: &PropertyKey) -> Option { + pub(crate) fn remove(&mut self, key: &PropertyKey) -> bool { self.properties.remove(key) } @@ -1848,28 +1860,19 @@ impl<'ctx, 'host> FunctionObjectBuilder<'ctx, 'host> { /// Build the function object. pub fn build(self) -> JsFunction { - let function = JsObject::from_proto_and_data( - self.context - .intrinsics() - .constructors() - .function() - .prototype(), - ObjectData::function(Function::new( - FunctionKind::Native { - function: self.function, - constructor: self.constructor, - }, - self.context.realm().clone(), - )), + let function = Function::new( + FunctionKind::Native { + function: self.function, + constructor: self.constructor, + }, + self.context.realm().clone(), + ); + let object = self.context.intrinsics().templates().function().create( + ObjectData::function(function), + vec![self.length.into(), self.name.into()], ); - let property = PropertyDescriptor::builder() - .writable(false) - .enumerable(false) - .configurable(true); - function.insert_property(utf16!("length"), property.clone().value(self.length)); - function.insert_property(utf16!("name"), property.value(self.name)); - JsFunction::from_object_unchecked(function) + JsFunction::from_object_unchecked(object) } } @@ -1917,7 +1920,8 @@ impl<'ctx, 'host> ObjectInitializer<'ctx, 'host> { /// Create a new `ObjectBuilder` with custom [`NativeObject`] data. pub fn with_native(data: T, context: &'ctx mut Context<'host>) -> Self { - let object = JsObject::from_proto_and_data( + let object = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), context.intrinsics().constructors().object().prototype(), ObjectData::native_object(data), ); @@ -2025,14 +2029,12 @@ impl<'ctx, 'host> ConstructorBuilder<'ctx, 'host> { constructor_object: Object { kind: ObjectKind::Ordinary, properties: PropertyMap::default(), - prototype: None, extensible: true, private_elements: ThinVec::new(), }, prototype: Object { kind: ObjectKind::Ordinary, properties: PropertyMap::default(), - prototype: None, extensible: true, private_elements: ThinVec::new(), }, diff --git a/boa_engine/src/object/property_map.rs b/boa_engine/src/object/property_map.rs index 374bd7e221..90ffb50989 100644 --- a/boa_engine/src/object/property_map.rs +++ b/boa_engine/src/object/property_map.rs @@ -1,4 +1,12 @@ -use super::{PropertyDescriptor, PropertyKey}; +use super::{ + shape::{ + property_table::PropertyTableInner, + shared_shape::TransitionKey, + slot::{Slot, SlotAttributes}, + ChangeTransitionAction, Shape, UniqueShape, + }, + JsPrototype, ObjectStorage, PropertyDescriptor, PropertyKey, +}; use crate::{property::PropertyDescriptorBuilder, JsString, JsSymbol, JsValue}; use boa_gc::{custom_trace, Finalize, Trace}; use indexmap::IndexMap; @@ -59,6 +67,10 @@ impl Default for IndexedProperties { } impl IndexedProperties { + fn new(elements: ThinVec) -> Self { + Self::Dense(elements) + } + /// Get a property descriptor if it exists. fn get(&self, key: u32) -> Option { match self { @@ -95,9 +107,9 @@ impl IndexedProperties { } /// Inserts a property descriptor with the specified key. - fn insert(&mut self, key: u32, property: PropertyDescriptor) -> Option { + fn insert(&mut self, key: u32, property: PropertyDescriptor) -> bool { let vec = match self { - Self::Sparse(map) => return map.insert(key, property), + Self::Sparse(map) => return map.insert(key, property).is_some(), Self::Dense(vec) => { let len = vec.len() as u32; if key <= len @@ -118,19 +130,12 @@ impl IndexedProperties { // Since the previous key is the current key - 1. Meaning that the elements are continuos. if key == len { vec.push(value); - return None; + return false; } // If it the key points in at a already taken index, swap and return it. std::mem::swap(&mut vec[key as usize], &mut value); - return Some( - PropertyDescriptorBuilder::new() - .writable(true) - .enumerable(true) - .configurable(true) - .value(value) - .build(), - ); + return true; } vec @@ -139,37 +144,32 @@ impl IndexedProperties { // Slow path: converting to sparse storage. let mut map = Self::convert_dense_to_sparse(vec); - let old_property = map.insert(key, property); + let replaced = map.insert(key, property).is_some(); *self = Self::Sparse(Box::new(map)); - old_property + replaced } /// Inserts a property descriptor with the specified key. - fn remove(&mut self, key: u32) -> Option { + fn remove(&mut self, key: u32) -> bool { let vec = match self { - Self::Sparse(map) => return map.remove(&key), + Self::Sparse(map) => { + return map.remove(&key).is_some(); + } Self::Dense(vec) => { // Fast Path: contiguous storage. // Has no elements or out of range, nothing to delete! if vec.is_empty() || key as usize >= vec.len() { - return None; + return false; } - // If the key is pointing at the last element, then we pop it and return it. + // If the key is pointing at the last element, then we pop it. // - // It does not make the storage sparse + // It does not make the storage sparse. if key as usize == vec.len().wrapping_sub(1) { - let value = vec.pop().expect("Already checked if it is out of bounds"); - return Some( - PropertyDescriptorBuilder::new() - .writable(true) - .enumerable(true) - .configurable(true) - .value(value) - .build(), - ); + vec.pop().expect("Already checked if it is out of bounds"); + return true; } vec @@ -178,10 +178,10 @@ impl IndexedProperties { // Slow Path: conversion to sparse storage. let mut map = Self::convert_dense_to_sparse(vec); - let old_property = map.remove(&key); + let removed = map.remove(&key).is_some(); *self = Self::Sparse(Box::new(map)); - old_property + removed } /// Check if we contain the key to a property descriptor. @@ -222,55 +222,185 @@ pub struct PropertyMap { /// Properties stored with integers as keys. indexed_properties: IndexedProperties, - /// Properties stored with `String`s a keys. - string_properties: OrderedHashMap, - - /// Properties stored with `Symbol`s a keys. - symbol_properties: OrderedHashMap, + pub(crate) shape: Shape, + pub(crate) storage: ObjectStorage, } impl PropertyMap { /// Create a new [`PropertyMap`]. #[must_use] #[inline] - pub fn new() -> Self { - Self::default() + pub fn new(shape: Shape, elements: ThinVec) -> Self { + Self { + indexed_properties: IndexedProperties::new(elements), + shape, + storage: Vec::default(), + } + } + + /// Construct a [`PropertyMap`] from with the given prototype with an unique [`Shape`]. + #[must_use] + #[inline] + pub fn from_prototype_unique_shape(prototype: JsPrototype) -> Self { + Self { + indexed_properties: IndexedProperties::default(), + shape: Shape::unique(UniqueShape::new(prototype, PropertyTableInner::default())), + storage: Vec::default(), + } + } + + /// Construct a [`PropertyMap`] from with the given prototype with a shared shape [`Shape`]. + #[must_use] + #[inline] + pub fn from_prototype_with_shared_shape(mut root_shape: Shape, prototype: JsPrototype) -> Self { + root_shape = root_shape.change_prototype_transition(prototype); + Self { + indexed_properties: IndexedProperties::default(), + shape: root_shape, + storage: Vec::default(), + } } /// Get the property with the given key from the [`PropertyMap`]. #[must_use] pub fn get(&self, key: &PropertyKey) -> Option { - match key { - PropertyKey::Index(index) => self.indexed_properties.get(*index), - PropertyKey::String(string) => self.string_properties.0.get(string).cloned(), - PropertyKey::Symbol(symbol) => self.symbol_properties.0.get(symbol).cloned(), + if let PropertyKey::Index(index) = key { + return self.indexed_properties.get(*index); } + if let Some(slot) = self.shape.lookup(key) { + return Some(self.get_storage(slot)); + } + + None + } + + /// Get the property with the given key from the [`PropertyMap`]. + #[must_use] + pub(crate) fn get_storage(&self, Slot { index, attributes }: Slot) -> PropertyDescriptor { + let index = index as usize; + let mut builder = PropertyDescriptor::builder() + .configurable(attributes.contains(SlotAttributes::CONFIGURABLE)) + .enumerable(attributes.contains(SlotAttributes::ENUMERABLE)); + if attributes.is_accessor_descriptor() { + if attributes.has_get() { + builder = builder.get(self.storage[index].clone()); + } + if attributes.has_set() { + builder = builder.set(self.storage[index + 1].clone()); + } + } else { + builder = builder.writable(attributes.contains(SlotAttributes::WRITABLE)); + builder = builder.value(self.storage[index].clone()); + } + builder.build() } /// Insert the given property descriptor with the given key [`PropertyMap`]. - pub fn insert( - &mut self, - key: &PropertyKey, - property: PropertyDescriptor, - ) -> Option { - match &key { - PropertyKey::Index(index) => self.indexed_properties.insert(*index, property), - PropertyKey::String(string) => { - self.string_properties.0.insert(string.clone(), property) + pub fn insert(&mut self, key: &PropertyKey, property: PropertyDescriptor) -> bool { + if let PropertyKey::Index(index) = key { + return self.indexed_properties.insert(*index, property); + } + + let attributes = property.to_slot_attributes(); + + if let Some(slot) = self.shape.lookup(key) { + let index = slot.index as usize; + + if slot.attributes != attributes { + let key = TransitionKey { + property_key: key.clone(), + attributes, + }; + let transition = self.shape.change_attributes_transition(key); + self.shape = transition.shape; + match transition.action { + ChangeTransitionAction::Nothing => {} + ChangeTransitionAction::Remove => { + self.storage.remove(slot.index as usize + 1); + } + ChangeTransitionAction::Insert => { + // insert after index which is (index + 1). + self.storage.insert(index, JsValue::undefined()); + } + } } - PropertyKey::Symbol(symbol) => { - self.symbol_properties.0.insert(symbol.clone(), property) + + if attributes.is_accessor_descriptor() { + if attributes.has_get() { + self.storage[index] = property + .get() + .cloned() + .map(JsValue::new) + .unwrap_or_default(); + } + if attributes.has_set() { + self.storage[index + 1] = property + .set() + .cloned() + .map(JsValue::new) + .unwrap_or_default(); + } + } else { + self.storage[index] = property.expect_value().clone(); } + return true; } + + let transition_key = TransitionKey { + property_key: key.clone(), + attributes, + }; + self.shape = self.shape.insert_property_transition(transition_key); + + // Make Sure that if we are inserting, it has the correct slot index. + debug_assert_eq!( + self.shape.lookup(key), + Some(Slot { + index: self.storage.len() as u32, + attributes + }) + ); + + if attributes.is_accessor_descriptor() { + self.storage.push( + property + .get() + .cloned() + .map(JsValue::new) + .unwrap_or_default(), + ); + self.storage.push( + property + .set() + .cloned() + .map(JsValue::new) + .unwrap_or_default(), + ); + } else { + self.storage + .push(property.value().cloned().unwrap_or_default()); + } + + false } /// Remove the property with the given key from the [`PropertyMap`]. - pub fn remove(&mut self, key: &PropertyKey) -> Option { - match key { - PropertyKey::Index(index) => self.indexed_properties.remove(*index), - PropertyKey::String(string) => self.string_properties.0.shift_remove(string), - PropertyKey::Symbol(symbol) => self.symbol_properties.0.shift_remove(symbol), + pub fn remove(&mut self, key: &PropertyKey) -> bool { + if let PropertyKey::Index(index) = key { + return self.indexed_properties.remove(*index); } + if let Some(slot) = self.shape.lookup(key) { + // shift all elements when removing. + if slot.attributes.is_accessor_descriptor() { + self.storage.remove(slot.index as usize + 1); + } + self.storage.remove(slot.index as usize); + + self.shape = self.shape.remove_property_transition(key); + return true; + } + + false } /// Overrides all the indexed properties, setting it to dense storage. @@ -296,65 +426,6 @@ impl PropertyMap { } } - /// An iterator visiting all key-value pairs in arbitrary order. The iterator element type is `(PropertyKey, &'a Property)`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn iter(&self) -> Iter<'_> { - Iter { - indexed_properties: self.indexed_properties.iter(), - string_properties: self.string_properties.0.iter(), - symbol_properties: self.symbol_properties.0.iter(), - } - } - - /// An iterator visiting all keys in arbitrary order. The iterator element type is `PropertyKey`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn keys(&self) -> Keys<'_> { - Keys(self.iter()) - } - - /// An iterator visiting all values in arbitrary order. The iterator element type is `&'a Property`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn values(&self) -> Values<'_> { - Values(self.iter()) - } - - /// An iterator visiting all symbol key-value pairs in arbitrary order. The iterator element type is `(&'a RcSymbol, &'a Property)`. - /// - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn symbol_properties(&self) -> SymbolProperties<'_> { - SymbolProperties(self.symbol_properties.0.iter()) - } - - /// An iterator visiting all symbol keys in arbitrary order. The iterator element type is `&'a RcSymbol`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn symbol_property_keys(&self) -> SymbolPropertyKeys<'_> { - SymbolPropertyKeys(self.symbol_properties.0.keys()) - } - - /// An iterator visiting all symbol values in arbitrary order. The iterator element type is `&'a Property`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn symbol_property_values(&self) -> SymbolPropertyValues<'_> { - SymbolPropertyValues(self.symbol_properties.0.values()) - } - /// An iterator visiting all indexed key-value pairs in arbitrary order. The iterator element type is `(&'a u32, &'a Property)`. /// /// This iterator does not recurse down the prototype chain. @@ -382,42 +453,18 @@ impl PropertyMap { self.indexed_properties.values() } - /// An iterator visiting all string key-value pairs in arbitrary order. The iterator element type is `(&'a RcString, &'a Property)`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn string_properties(&self) -> StringProperties<'_> { - StringProperties(self.string_properties.0.iter()) - } - - /// An iterator visiting all string keys in arbitrary order. The iterator element type is `&'a RcString`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn string_property_keys(&self) -> StringPropertyKeys<'_> { - StringPropertyKeys(self.string_properties.0.keys()) - } - - /// An iterator visiting all string values in arbitrary order. The iterator element type is `&'a Property`. - /// - /// This iterator does not recurse down the prototype chain. - #[inline] - #[must_use] - pub fn string_property_values(&self) -> StringPropertyValues<'_> { - StringPropertyValues(self.string_properties.0.values()) - } - /// Returns `true` if the given key is contained in the [`PropertyMap`]. #[inline] #[must_use] pub fn contains_key(&self, key: &PropertyKey) -> bool { - match key { - PropertyKey::Index(index) => self.indexed_properties.contains_key(*index), - PropertyKey::String(string) => self.string_properties.0.contains_key(string), - PropertyKey::Symbol(symbol) => self.symbol_properties.0.contains_key(symbol), + if let PropertyKey::Index(index) = key { + return self.indexed_properties.contains_key(*index); + } + if self.shape.lookup(key).is_some() { + return true; } + + false } } @@ -450,131 +497,6 @@ impl ExactSizeIterator for Iter<'_> { } } -impl FusedIterator for Iter<'_> {} - -/// An iterator over the keys (`PropertyKey`) of an `Object`. -#[derive(Debug, Clone)] -pub struct Keys<'a>(Iter<'a>); - -impl Iterator for Keys<'_> { - type Item = PropertyKey; - fn next(&mut self) -> Option { - let (key, _) = self.0.next()?; - Some(key) - } -} - -impl ExactSizeIterator for Keys<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for Keys<'_> {} - -/// An iterator over the values (`Property`) of an `Object`. -#[derive(Debug, Clone)] -pub struct Values<'a>(Iter<'a>); - -impl Iterator for Values<'_> { - type Item = PropertyDescriptor; - fn next(&mut self) -> Option { - let (_, value) = self.0.next()?; - Some(value) - } -} - -impl ExactSizeIterator for Values<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for Values<'_> {} - -/// An iterator over the `Symbol` property entries of an `Object` -#[derive(Debug, Clone)] -pub struct SymbolProperties<'a>(indexmap::map::Iter<'a, JsSymbol, PropertyDescriptor>); - -impl<'a> Iterator for SymbolProperties<'a> { - type Item = (&'a JsSymbol, &'a PropertyDescriptor); - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl ExactSizeIterator for SymbolProperties<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for SymbolProperties<'_> {} - -/// An iterator over the keys (`RcSymbol`) of an `Object`. -#[derive(Debug, Clone)] -pub struct SymbolPropertyKeys<'a>(indexmap::map::Keys<'a, JsSymbol, PropertyDescriptor>); - -impl<'a> Iterator for SymbolPropertyKeys<'a> { - type Item = &'a JsSymbol; - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl ExactSizeIterator for SymbolPropertyKeys<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for SymbolPropertyKeys<'_> {} - -/// An iterator over the `Symbol` values (`Property`) of an `Object`. -#[derive(Debug, Clone)] -pub struct SymbolPropertyValues<'a>(indexmap::map::Values<'a, JsSymbol, PropertyDescriptor>); - -impl<'a> Iterator for SymbolPropertyValues<'a> { - type Item = &'a PropertyDescriptor; - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl ExactSizeIterator for SymbolPropertyValues<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for SymbolPropertyValues<'_> {} - /// An iterator over the indexed property entries of an `Object`. #[derive(Debug, Clone)] pub enum IndexProperties<'a> { @@ -713,86 +635,3 @@ impl ExactSizeIterator for IndexPropertyValues<'_> { } } } - -impl FusedIterator for IndexPropertyValues<'_> {} - -/// An iterator over the `String` property entries of an `Object` -#[derive(Debug, Clone)] -pub struct StringProperties<'a>(indexmap::map::Iter<'a, JsString, PropertyDescriptor>); - -impl<'a> Iterator for StringProperties<'a> { - type Item = (&'a JsString, &'a PropertyDescriptor); - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl ExactSizeIterator for StringProperties<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for StringProperties<'_> {} - -/// An iterator over the string keys (`RcString`) of an `Object`. -#[derive(Debug, Clone)] -pub struct StringPropertyKeys<'a>(indexmap::map::Keys<'a, JsString, PropertyDescriptor>); - -impl<'a> Iterator for StringPropertyKeys<'a> { - type Item = &'a JsString; - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl ExactSizeIterator for StringPropertyKeys<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for StringPropertyKeys<'_> {} - -/// An iterator over the string values (`Property`) of an `Object`. -#[derive(Debug, Clone)] -pub struct StringPropertyValues<'a>(indexmap::map::Values<'a, JsString, PropertyDescriptor>); - -impl<'a> Iterator for StringPropertyValues<'a> { - type Item = &'a PropertyDescriptor; - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl ExactSizeIterator for StringPropertyValues<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -impl FusedIterator for StringPropertyValues<'_> {} diff --git a/boa_engine/src/object/shape/mod.rs b/boa_engine/src/object/shape/mod.rs new file mode 100644 index 0000000000..0d7f6e8c13 --- /dev/null +++ b/boa_engine/src/object/shape/mod.rs @@ -0,0 +1,217 @@ +//! Implements object shapes. + +pub(crate) mod property_table; +pub(crate) mod shared_shape; +pub(crate) mod slot; +pub(crate) mod unique_shape; + +pub use shared_shape::SharedShape; +pub(crate) use unique_shape::UniqueShape; + +use std::fmt::Debug; + +use boa_gc::{Finalize, Trace}; + +use crate::property::PropertyKey; + +use self::{shared_shape::TransitionKey, slot::Slot}; + +use super::JsPrototype; + +/// Action to be performed after a property attribute change +// +// Example: of { get/set x() { ... }, y: ... } into { x: ..., y: ... } +// +// 0 1 2 +// Storage: | get x | set x | y | +// +// We delete at position of x which is index 0 (it spans two elements) + 1: +// +// 0 1 +// Storage: | x | y | +pub(crate) enum ChangeTransitionAction { + /// Do nothing to storage. + Nothing, + + /// Remove element at (index + 1) from storage. + Remove, + + /// Insert element at (index + 1) into storage. + Insert, +} + +/// The result of a change property attribute transition. +pub(crate) struct ChangeTransition { + /// The shape after transition. + pub(crate) shape: T, + + /// The needed action to be performed after transition to the object storage. + pub(crate) action: ChangeTransitionAction, +} + +/// The internal representation of [`Shape`]. +#[derive(Debug, Trace, Finalize, Clone)] +enum Inner { + Unique(UniqueShape), + Shared(SharedShape), +} + +/// Represents the shape of an object. +#[derive(Debug, Trace, Finalize, Clone)] +pub struct Shape { + inner: Inner, +} + +impl Default for Shape { + fn default() -> Self { + Shape::unique(UniqueShape::default()) + } +} + +impl Shape { + /// The max transition count of a [`SharedShape`] from the root node, + /// before the shape will be converted into a [`UniqueShape`] + /// + /// NOTE: This only applies to [`SharedShape`]. + const TRANSITION_COUNT_MAX: u16 = 1024; + + /// Create a [`Shape`] from a [`SharedShape`]. + pub(crate) fn shared(inner: SharedShape) -> Self { + Self { + inner: Inner::Shared(inner), + } + } + + /// Create a [`Shape`] from a [`UniqueShape`]. + pub(crate) const fn unique(shape: UniqueShape) -> Self { + Self { + inner: Inner::Unique(shape), + } + } + + /// Returns `true` if it's a shared shape, `false` otherwise. + #[inline] + pub const fn is_shared(&self) -> bool { + matches!(self.inner, Inner::Shared(_)) + } + + /// Returns `true` if it's a unique shape, `false` otherwise. + #[inline] + pub const fn is_unique(&self) -> bool { + matches!(self.inner, Inner::Unique(_)) + } + + pub(crate) const fn as_unique(&self) -> Option<&UniqueShape> { + if let Inner::Unique(shape) = &self.inner { + return Some(shape); + } + None + } + + /// Create an insert property transitions returning the new transitioned [`Shape`]. + /// + /// NOTE: This assumes that there is no property with the given key! + pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { + match &self.inner { + Inner::Shared(shape) => { + let shape = shape.insert_property_transition(key); + if shape.transition_count() >= Self::TRANSITION_COUNT_MAX { + return Self::unique(shape.to_unique()); + } + Self::shared(shape) + } + Inner::Unique(shape) => Self::unique(shape.insert_property_transition(key)), + } + } + + /// Create a change attribute property transitions returning [`ChangeTransition`] containing the new [`Shape`] + /// and actions to be performed + /// + /// NOTE: This assumes that there already is a property with the given key! + pub(crate) fn change_attributes_transition( + &self, + key: TransitionKey, + ) -> ChangeTransition { + match &self.inner { + Inner::Shared(shape) => { + let change_transition = shape.change_attributes_transition(key); + let shape = + if change_transition.shape.transition_count() >= Self::TRANSITION_COUNT_MAX { + Self::unique(change_transition.shape.to_unique()) + } else { + Self::shared(change_transition.shape) + }; + ChangeTransition { + shape, + action: change_transition.action, + } + } + Inner::Unique(shape) => shape.change_attributes_transition(&key), + } + } + + /// Remove a property property from the [`Shape`] returning the new transitioned [`Shape`]. + /// + /// NOTE: This assumes that there already is a property with the given key! + pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { + match &self.inner { + Inner::Shared(shape) => { + let shape = shape.remove_property_transition(key); + if shape.transition_count() >= Self::TRANSITION_COUNT_MAX { + return Self::unique(shape.to_unique()); + } + Self::shared(shape) + } + Inner::Unique(shape) => Self::unique(shape.remove_property_transition(key)), + } + } + + /// Create a prototype transitions returning the new transitioned [`Shape`]. + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + match &self.inner { + Inner::Shared(shape) => { + let shape = shape.change_prototype_transition(prototype); + if shape.transition_count() >= Self::TRANSITION_COUNT_MAX { + return Self::unique(shape.to_unique()); + } + Self::shared(shape) + } + Inner::Unique(shape) => Self::unique(shape.change_prototype_transition(prototype)), + } + } + + /// Get the [`JsPrototype`] of the [`Shape`]. + pub fn prototype(&self) -> JsPrototype { + match &self.inner { + Inner::Shared(shape) => shape.prototype(), + Inner::Unique(shape) => shape.prototype(), + } + } + + /// Lookup a property in the shape + #[inline] + pub(crate) fn lookup(&self, key: &PropertyKey) -> Option { + match &self.inner { + Inner::Shared(shape) => shape.lookup(key), + Inner::Unique(shape) => shape.lookup(key), + } + } + + /// Returns the keys of the [`Shape`], in insertion order. + #[inline] + pub fn keys(&self) -> Vec { + match &self.inner { + Inner::Shared(shape) => shape.keys(), + Inner::Unique(shape) => shape.keys(), + } + } + + /// Return location in memory of the [`Shape`]. + #[inline] + pub fn to_addr_usize(&self) -> usize { + match &self.inner { + Inner::Shared(shape) => shape.to_addr_usize(), + Inner::Unique(shape) => shape.to_addr_usize(), + } + } +} diff --git a/boa_engine/src/object/shape/property_table.rs b/boa_engine/src/object/shape/property_table.rs new file mode 100644 index 0000000000..cb2d751304 --- /dev/null +++ b/boa_engine/src/object/shape/property_table.rs @@ -0,0 +1,149 @@ +use std::{cell::RefCell, rc::Rc}; + +use rustc_hash::FxHashMap; + +use crate::{ + object::shape::slot::{Slot, SlotAttributes}, + property::PropertyKey, +}; + +/// The internal representation of [`PropertyTable`]. +#[derive(Default, Debug, Clone)] +pub(crate) struct PropertyTableInner { + pub(crate) map: FxHashMap, + pub(crate) keys: Vec<(PropertyKey, Slot)>, +} + +impl PropertyTableInner { + /// Returns all the keys, in insertion order. + pub(crate) fn keys(&self) -> Vec { + self.keys_cloned_n(self.keys.len() as u32) + } + + /// Returns `n` cloned keys, in insertion order. + pub(crate) fn keys_cloned_n(&self, n: u32) -> Vec { + let n = n as usize; + + self.keys + .iter() + .take(n) + .map(|(key, _)| key) + .filter(|key| matches!(key, PropertyKey::String(_))) + .chain( + self.keys + .iter() + .take(n) + .map(|(key, _)| key) + .filter(|key| matches!(key, PropertyKey::Symbol(_))), + ) + .cloned() + .collect() + } + + /// Returns a new table with `n` cloned properties. + pub(crate) fn clone_count(&self, n: u32) -> Self { + let n = n as usize; + + let mut keys = Vec::with_capacity(n); + let mut map = FxHashMap::default(); + + for (index, (key, slot)) in self.keys.iter().take(n).enumerate() { + keys.push((key.clone(), *slot)); + map.insert(key.clone(), (index as u32, *slot)); + } + + Self { map, keys } + } + + /// Insert a property entry into the table. + pub(crate) fn insert(&mut self, key: PropertyKey, attributes: SlotAttributes) { + let slot = Slot::from_previous(self.keys.last().map(|x| x.1), attributes); + let index = self.keys.len() as u32; + self.keys.push((key.clone(), slot)); + let value = self.map.insert(key, (index, slot)); + debug_assert!(value.is_none()); + } +} + +/// Represents an ordered property table, that maps [`PropertyTable`] to [`Slot`]. +/// +/// This is shared between [`crate::object::shape::SharedShape`]. +#[derive(Default, Debug, Clone)] +pub(crate) struct PropertyTable { + pub(super) inner: Rc>, +} + +impl PropertyTable { + /// Returns the inner representation of a [`PropertyTable`]. + pub(super) fn inner(&self) -> &RefCell { + &self.inner + } + + /// Add a property to the [`PropertyTable`] or deep clone it, + /// if there already is a property or the property has attributes that are not the same. + pub(crate) fn add_property_deep_clone_if_needed( + &self, + key: PropertyKey, + attributes: SlotAttributes, + property_count: u32, + ) -> Self { + { + let mut inner = self.inner.borrow_mut(); + if (property_count as usize) == inner.keys.len() && !inner.map.contains_key(&key) { + inner.insert(key, attributes); + return self.clone(); + } + } + + // property is already present need to make deep clone of property table. + let this = self.deep_clone(property_count); + { + let mut inner = this.inner.borrow_mut(); + inner.insert(key, attributes); + } + this + } + + /// Deep clone the [`PropertyTable`] in insertion order with the first n properties. + pub(crate) fn deep_clone(&self, n: u32) -> Self { + Self { + inner: Rc::new(RefCell::new(self.inner.borrow().clone_count(n))), + } + } + + /// Deep clone the [`PropertyTable`]. + pub(crate) fn deep_clone_all(&self) -> Self { + Self { + inner: Rc::new(RefCell::new((*self.inner.borrow()).clone())), + } + } + + /// Change the attributes of a property. + pub(crate) fn set_attributes_at_index( + &self, + key: &PropertyKey, + property_attributes: SlotAttributes, + ) { + let mut inner = self.inner.borrow_mut(); + let Some((index, slot)) = inner.map.get_mut(key) else { + unreachable!("There should already be a property!") + }; + slot.attributes = property_attributes; + let index = *index as usize; + + inner.keys[index].1.attributes = property_attributes; + } + + /// Get a property from the [`PropertyTable`]. + /// + /// Panics: + /// + /// If it is not in the [`PropertyTable`]. + pub(crate) fn get_expect(&self, key: &PropertyKey) -> Slot { + let inner = self.inner.borrow(); + let Some((_, slot)) = inner.map.get(key) else { + unreachable!("There should already be a property!") + }; + *slot + } +} diff --git a/boa_engine/src/object/shape/shared_shape/forward_transition.rs b/boa_engine/src/object/shape/shared_shape/forward_transition.rs new file mode 100644 index 0000000000..ba9c9488c8 --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/forward_transition.rs @@ -0,0 +1,58 @@ +use boa_gc::{Finalize, Gc, GcRefCell, Trace, WeakGc}; +use rustc_hash::FxHashMap; + +use crate::object::JsPrototype; + +use super::{Inner as SharedShapeInner, TransitionKey}; + +/// Maps transition key type to a [`SharedShapeInner`] transition. +type TransitionMap = FxHashMap>; + +/// The internal representation of [`ForwardTransition`]. +#[derive(Default, Debug, Trace, Finalize)] +struct Inner { + properties: Option>>, + prototypes: Option>>, +} + +/// Holds a forward reference to a previously created transition. +/// +/// The reference is weak, therefore it can be garbage collected if it is not in use. +#[derive(Default, Debug, Trace, Finalize)] +pub(super) struct ForwardTransition { + inner: GcRefCell, +} + +impl ForwardTransition { + /// Insert a property transition. + pub(super) fn insert_property(&self, key: TransitionKey, value: &Gc) { + let mut this = self.inner.borrow_mut(); + let properties = this.properties.get_or_insert_with(Box::default); + properties.insert(key, WeakGc::new(value)); + } + + /// Insert a prototype transition. + pub(super) fn insert_prototype(&self, key: JsPrototype, value: &Gc) { + let mut this = self.inner.borrow_mut(); + let prototypes = this.prototypes.get_or_insert_with(Box::default); + prototypes.insert(key, WeakGc::new(value)); + } + + /// Get a property transition, return [`None`] otherwise. + pub(super) fn get_property(&self, key: &TransitionKey) -> Option> { + let this = self.inner.borrow(); + let Some(transitions) = this.properties.as_ref() else { + return None; + }; + transitions.get(key).cloned() + } + + /// Get a prototype transition, return [`None`] otherwise. + pub(super) fn get_prototype(&self, key: &JsPrototype) -> Option> { + let this = self.inner.borrow(); + let Some(transitions) = this.prototypes.as_ref() else { + return None; + }; + transitions.get(key).cloned() + } +} diff --git a/boa_engine/src/object/shape/shared_shape/mod.rs b/boa_engine/src/object/shape/shared_shape/mod.rs new file mode 100644 index 0000000000..0908bc1235 --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/mod.rs @@ -0,0 +1,469 @@ +mod forward_transition; +pub(crate) mod template; + +use std::{collections::hash_map::RandomState, hash::Hash}; + +use bitflags::bitflags; +use boa_gc::{empty_trace, Finalize, Gc, Trace}; +use indexmap::IndexMap; + +use crate::{object::JsPrototype, property::PropertyKey, JsObject}; + +use self::forward_transition::ForwardTransition; + +use super::{ + property_table::PropertyTable, slot::SlotAttributes, ChangeTransition, ChangeTransitionAction, + Slot, UniqueShape, +}; + +/// Represent a [`SharedShape`] property transition. +#[derive(Debug, Finalize, Clone, PartialEq, Eq, Hash)] +pub(crate) struct TransitionKey { + pub(crate) property_key: PropertyKey, + pub(crate) attributes: SlotAttributes, +} + +// SAFETY: Non of the member of this struct are garbage collected, +// so this should be fine. +unsafe impl Trace for TransitionKey { + empty_trace!(); +} + +const INSERT_PROPERTY_TRANSITION_TYPE: u8 = 0b0000_0000; +const CONFIGURE_PROPERTY_TRANSITION_TYPE: u8 = 0b0000_0001; +const PROTOTYPE_TRANSITION_TYPE: u8 = 0b0000_0010; + +// Reserved for future use! +#[allow(unused)] +const RESEREVED_TRANSITION_TYPE: u8 = 0b0000_0011; + +bitflags! { + /// Flags of a shape. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Finalize)] + pub struct ShapeFlags: u8 { + /// Represents the transition type of a [`SharedShape`]. + const TRANSITION_TYPE = 0b0000_0011; + } +} + +impl Default for ShapeFlags { + fn default() -> Self { + Self::empty() + } +} + +impl ShapeFlags { + // NOTE: Remove type bits and set the new ones. + fn insert_property_transition_from(previous: ShapeFlags) -> Self { + previous.difference(Self::TRANSITION_TYPE) + | Self::from_bits_retain(INSERT_PROPERTY_TRANSITION_TYPE) + } + fn configure_property_transition_from(previous: ShapeFlags) -> Self { + previous.difference(Self::TRANSITION_TYPE) + | Self::from_bits_retain(CONFIGURE_PROPERTY_TRANSITION_TYPE) + } + fn prototype_transition_from(previous: ShapeFlags) -> Self { + previous.difference(Self::TRANSITION_TYPE) + | Self::from_bits_retain(PROTOTYPE_TRANSITION_TYPE) + } + + const fn is_insert_transition_type(self) -> bool { + self.intersection(Self::TRANSITION_TYPE).bits() == INSERT_PROPERTY_TRANSITION_TYPE + } + const fn is_prototype_transition_type(self) -> bool { + self.intersection(Self::TRANSITION_TYPE).bits() == PROTOTYPE_TRANSITION_TYPE + } +} + +// SAFETY: Non of the member of this struct are garbage collected, +// so this should be fine. +unsafe impl Trace for ShapeFlags { + empty_trace!(); +} + +/// The internal representation of a [`SharedShape`]. +#[derive(Debug, Trace, Finalize)] +struct Inner { + /// See [`ForwardTransition`]. + forward_transitions: ForwardTransition, + + /// The count of how many properties this [`SharedShape`] holds. + property_count: u32, + + /// Instance prototype `__proto__`. + prototype: JsPrototype, + + // SAFETY: This is safe because nothing in [`PropertyTable`] + // needs tracing + #[unsafe_ignore_trace] + property_table: PropertyTable, + + /// The previous shape in the transition chain. + /// + /// [`None`] if it is the root shape. + previous: Option, + + /// How many transitions have happened from the root node. + transition_count: u16, + + /// Flags about the shape. + flags: ShapeFlags, +} + +/// Represents a shared object shape. +#[derive(Debug, Trace, Finalize, Clone)] +pub struct SharedShape { + inner: Gc, +} + +impl SharedShape { + fn property_table(&self) -> &PropertyTable { + &self.inner.property_table + } + /// Return the property count that this shape owns in the [`PropertyTable`]. + fn property_count(&self) -> u32 { + self.inner.property_count + } + /// Return the index to the property in the the [`PropertyTable`]. + fn property_index(&self) -> u32 { + self.inner.property_count.saturating_sub(1) + } + /// Getter for the transition count field. + pub fn transition_count(&self) -> u16 { + self.inner.transition_count + } + /// Getter for the previous field. + pub fn previous(&self) -> Option<&SharedShape> { + self.inner.previous.as_ref() + } + /// Get the prototype of the shape. + pub fn prototype(&self) -> JsPrototype { + self.inner.prototype.clone() + } + /// Get the property this [`SharedShape`] refers to. + pub(crate) fn property(&self) -> (PropertyKey, Slot) { + let inner = self.property_table().inner().borrow(); + let (key, slot) = inner + .keys + .get(self.property_index() as usize) + .expect("There should be a property"); + (key.clone(), *slot) + } + /// Get the flags of the shape. + fn flags(&self) -> ShapeFlags { + self.inner.flags + } + /// Getter for the [`ForwardTransition`] field. + fn forward_transitions(&self) -> &ForwardTransition { + &self.inner.forward_transitions + } + /// Check if the shape has the given prototype. + pub fn has_prototype(&self, prototype: &JsObject) -> bool { + self.inner + .prototype + .as_ref() + .map_or(false, |this| this == prototype) + } + + /// Create a new [`SharedShape`]. + fn new(inner: Inner) -> Self { + Self { + inner: Gc::new(inner), + } + } + + /// Create a root [`SharedShape`]. + #[must_use] + pub fn root() -> Self { + Self::new(Inner { + forward_transitions: ForwardTransition::default(), + prototype: None, + property_count: 0, + property_table: PropertyTable::default(), + previous: None, + flags: ShapeFlags::default(), + transition_count: 0, + }) + } + + /// Create a [`SharedShape`] change prototype transition. + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + if let Some(shape) = self.forward_transitions().get_prototype(&prototype) { + if let Some(inner) = shape.upgrade() { + return Self { inner }; + } + } + let new_inner_shape = Inner { + forward_transitions: ForwardTransition::default(), + prototype: prototype.clone(), + property_table: self.property_table().clone(), + property_count: self.property_count(), + previous: Some(self.clone()), + transition_count: self.transition_count() + 1, + flags: ShapeFlags::prototype_transition_from(self.flags()), + }; + let new_shape = Self::new(new_inner_shape); + + self.forward_transitions() + .insert_prototype(prototype, &new_shape.inner); + + new_shape + } + + /// Create a [`SharedShape`] insert property transition. + pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { + // Check if we have already created such a transition, if so use it! + if let Some(shape) = self.forward_transitions().get_property(&key) { + if let Some(inner) = shape.upgrade() { + return Self { inner }; + } + } + + let property_table = self.property_table().add_property_deep_clone_if_needed( + key.property_key.clone(), + key.attributes, + self.property_count(), + ); + let new_inner_shape = Inner { + prototype: self.prototype(), + forward_transitions: ForwardTransition::default(), + property_table, + property_count: self.property_count() + 1, + previous: Some(self.clone()), + transition_count: self.transition_count() + 1, + flags: ShapeFlags::insert_property_transition_from(self.flags()), + }; + let new_shape = Self::new(new_inner_shape); + + self.forward_transitions() + .insert_property(key, &new_shape.inner); + + new_shape + } + + /// Create a [`SharedShape`] change prototype transition, returning [`ChangeTransition`]. + pub(crate) fn change_attributes_transition( + &self, + key: TransitionKey, + ) -> ChangeTransition { + let slot = self.property_table().get_expect(&key.property_key); + + // Check if we have already created such a transition, if so use it! + if let Some(shape) = self.forward_transitions().get_property(&key) { + if let Some(inner) = shape.upgrade() { + let action = if slot.attributes.width_match(key.attributes) { + ChangeTransitionAction::Nothing + } else if slot.attributes.is_accessor_descriptor() { + // Accessor property --> Data property + ChangeTransitionAction::Remove + } else { + // Data property --> Accessor property + ChangeTransitionAction::Insert + }; + + return ChangeTransition { + shape: Self { inner }, + action, + }; + } + } + + // The attribute change transitions, didn't change from accessor to data property or vice-versa. + if slot.attributes.width_match(key.attributes) { + let property_table = self.property_table().deep_clone_all(); + property_table.set_attributes_at_index(&key.property_key, key.attributes); + let inner_shape = Inner { + forward_transitions: ForwardTransition::default(), + prototype: self.prototype(), + property_table, + property_count: self.property_count(), + previous: Some(self.clone()), + transition_count: self.transition_count() + 1, + flags: ShapeFlags::configure_property_transition_from(self.flags()), + }; + let shape = Self::new(inner_shape); + + self.forward_transitions() + .insert_property(key, &shape.inner); + + return ChangeTransition { + shape, + action: ChangeTransitionAction::Nothing, + }; + } + + // Rollback before the property has added. + let (mut base, prototype, transitions) = self.rollback_before(&key.property_key); + + // Apply prototype transition, if it was found. + if let Some(prototype) = prototype { + base = base.change_prototype_transition(prototype); + } + + // Apply this property. + base = base.insert_property_transition(key); + + // Apply previous properties. + for (property_key, attributes) in transitions.into_iter().rev() { + let transition = TransitionKey { + property_key, + attributes, + }; + base = base.insert_property_transition(transition); + } + + // Determine action to be performed on the storage. + let action = if slot.attributes.is_accessor_descriptor() { + // Accessor property --> Data property + ChangeTransitionAction::Remove + } else { + // Data property --> Accessor property + ChangeTransitionAction::Insert + }; + + ChangeTransition { + shape: base, + action, + } + } + + /// Rollback to shape before the insertion of the [`PropertyKey`] that is provided. + /// + /// This returns the shape before the insertion, if it sees a prototype transition it will return the lastest one, + /// ignoring any others, [`None`] otherwise. It also will return the property transitions ordered from + /// latest to oldest that it sees. + /// + /// NOTE: In the transitions it does not include the property that we are rolling back. + /// + /// NOTE: The prototype transitions if it sees a property insert and then later an attribute change it will condense + /// into one property insert transition with the new attribute in the change attribute transition, + /// in the same place that the property was inserted initially. + // + // For example with the following chain: + // + // INSERT(x) INSERT(y) INSERT(z) + // { } ------------> { x } ------------> { x, y } ------------> { x, y, z } + // + // Then we call rollback on `y`: + // + // INSERT(x) INSERT(y) INSERT(z) + // { } ------------> { x } ------------> { x, y } ------------> { x, y, z } + // ^ + // \--- base (with array of transitions to be performed: INSERT(z), + // and protortype: None ) + fn rollback_before( + &self, + key: &PropertyKey, + ) -> ( + SharedShape, + Option, + IndexMap, + ) { + let mut prototype = None; + let mut transitions: IndexMap = + IndexMap::default(); + + let mut current = Some(self); + let base = loop { + let Some(current_shape) = current else { + unreachable!("The chain should have insert transition type!") + }; + + // We only take the latest prototype change it, if it exists. + if current_shape.flags().is_prototype_transition_type() { + if prototype.is_none() { + prototype = Some(current_shape.prototype().clone()); + } + + // Skip when it is a prototype transition. + current = current_shape.previous(); + continue; + } + + let (current_property_key, slot) = current_shape.property(); + + if current_shape.flags().is_insert_transition_type() && ¤t_property_key == key { + let base = if let Some(base) = current_shape.previous() { + base.clone() + } else { + // It's the root, because it doesn't have previous. + current_shape.clone() + }; + break base; + } + + // Do not add property that we are trying to delete. + // this can happen if a configure was called after inserting it into the shape + if ¤t_property_key != key { + // Only take the latest changes to a property. To try to build a smaller tree. + transitions + .entry(current_property_key) + .or_insert(slot.attributes); + } + + current = current_shape.previous(); + }; + + (base, prototype, transitions) + } + + /// Remove a property from [`SharedShape`], returning the new [`SharedShape`]. + pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { + let (mut base, prototype, transitions) = self.rollback_before(key); + + // Apply prototype transition, if it was found. + if let Some(prototype) = prototype { + base = base.change_prototype_transition(prototype); + } + + for (property_key, attributes) in transitions.into_iter().rev() { + let transition = TransitionKey { + property_key, + attributes, + }; + base = base.insert_property_transition(transition); + } + + base + } + + /// Do a property lookup, returns [`None`] if property not found. + pub(crate) fn lookup(&self, key: &PropertyKey) -> Option { + let property_count = self.property_count(); + if property_count == 0 { + return None; + } + + let property_table_inner = self.property_table().inner().borrow(); + if let Some((property_table_index, slot)) = property_table_inner.map.get(key) { + // Check if we are trying to access properties that belong to another shape. + if *property_table_index < self.property_count() { + return Some(*slot); + } + } + None + } + + /// Gets all keys first strings then symbols in creation order. + pub(crate) fn keys(&self) -> Vec { + let property_table = self.property_table().inner().borrow(); + property_table.keys_cloned_n(self.property_count()) + } + + /// Returns a new [`UniqueShape`] with the properties of the [`SharedShape`]. + pub(crate) fn to_unique(&self) -> UniqueShape { + UniqueShape::new( + self.prototype(), + self.property_table() + .inner() + .borrow() + .clone_count(self.property_count()), + ) + } + + /// Return location in memory of the [`UniqueShape`]. + pub(crate) fn to_addr_usize(&self) -> usize { + let ptr: *const _ = self.inner.as_ref(); + ptr as usize + } +} diff --git a/boa_engine/src/object/shape/shared_shape/template.rs b/boa_engine/src/object/shape/shared_shape/template.rs new file mode 100644 index 0000000000..9e30d12901 --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/template.rs @@ -0,0 +1,141 @@ +use boa_gc::{Finalize, Trace}; +use thin_vec::ThinVec; + +use crate::{ + object::{ + shape::{slot::SlotAttributes, Shape}, + JsObject, Object, ObjectData, PropertyMap, + }, + property::{Attribute, PropertyKey}, + JsValue, +}; + +use super::{SharedShape, TransitionKey}; + +/// Represent a template of an objects properties and prototype. +/// This is used to construct as many objects as needed from a predefined [`SharedShape`]. +#[derive(Debug, Clone, Trace, Finalize)] +pub(crate) struct ObjectTemplate { + shape: SharedShape, +} + +impl ObjectTemplate { + /// Create a new [`ObjectTemplate`] + pub(crate) fn new(root_shape: &SharedShape) -> Self { + Self { + shape: root_shape.clone(), + } + } + + /// Create and [`ObjectTemplate`] with a prototype. + pub(crate) fn with_prototype(root_shape: &SharedShape, prototype: JsObject) -> Self { + let shape = root_shape.change_prototype_transition(Some(prototype)); + Self { shape } + } + + /// Check if the shape has a specific, prototype. + pub(crate) fn has_prototype(&self, prototype: &JsObject) -> bool { + self.shape.has_prototype(prototype) + } + + /// Set the prototype of the [`ObjectTemplate`]. + /// + /// This assumes that the prototype has not been set yet. + pub(crate) fn set_prototype(&mut self, prototype: JsObject) -> &mut Self { + self.shape = self.shape.change_prototype_transition(Some(prototype)); + self + } + + /// Add a data property to the [`ObjectTemplate`]. + /// + /// This assumes that the property with the given key was not previously set + /// and that it's a string or symbol. + pub(crate) fn property(&mut self, key: PropertyKey, attributes: Attribute) -> &mut Self { + debug_assert!(!matches!(&key, PropertyKey::Index(_))); + + let attributes = SlotAttributes::from_bits_truncate(attributes.bits()); + self.shape = self.shape.insert_property_transition(TransitionKey { + property_key: key, + attributes, + }); + + self + } + + /// Add a accessor property to the [`ObjectTemplate`]. + /// + /// This assumes that the property with the given key was not previously set + /// and that it's a string or symbol. + pub(crate) fn accessor( + &mut self, + key: PropertyKey, + get: bool, + set: bool, + attributes: Attribute, + ) -> &mut Self { + // TOOD: We don't support indexed keys. + debug_assert!(!matches!(&key, PropertyKey::Index(_))); + + let attributes = { + let mut result = SlotAttributes::empty(); + result.set( + SlotAttributes::CONFIGURABLE, + attributes.contains(Attribute::CONFIGURABLE), + ); + result.set( + SlotAttributes::ENUMERABLE, + attributes.contains(Attribute::ENUMERABLE), + ); + + result.set(SlotAttributes::GET, get); + result.set(SlotAttributes::SET, set); + + result + }; + + self.shape = self.shape.insert_property_transition(TransitionKey { + property_key: key, + attributes, + }); + + self + } + + /// Create an object from the [`ObjectTemplate`] + /// + /// The storage must match the properties provided. + pub(crate) fn create(&self, data: ObjectData, storage: Vec) -> JsObject { + let mut object = Object { + kind: data.kind, + extensible: true, + properties: PropertyMap::new(Shape::shared(self.shape.clone()), ThinVec::default()), + private_elements: ThinVec::new(), + }; + + object.properties.storage = storage; + + JsObject::from_object_and_vtable(object, data.internal_methods) + } + + /// Create an object from the [`ObjectTemplate`] + /// + /// The storage must match the properties provided. It does not apply to + /// the indexed propeties. + pub(crate) fn create_with_indexed_properties( + &self, + data: ObjectData, + storage: Vec, + elements: ThinVec, + ) -> JsObject { + let mut object = Object { + kind: data.kind, + extensible: true, + properties: PropertyMap::new(Shape::shared(self.shape.clone()), elements), + private_elements: ThinVec::new(), + }; + + object.properties.storage = storage; + + JsObject::from_object_and_vtable(object, data.internal_methods) + } +} diff --git a/boa_engine/src/object/shape/slot.rs b/boa_engine/src/object/shape/slot.rs new file mode 100644 index 0000000000..b219d08fd2 --- /dev/null +++ b/boa_engine/src/object/shape/slot.rs @@ -0,0 +1,77 @@ +use bitflags::bitflags; +pub(crate) type SlotIndex = u32; + +bitflags! { + /// Attributes of a slot. + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(crate) struct SlotAttributes: u8 { + const WRITABLE = 0b0000_0001; + const ENUMERABLE = 0b0000_0010; + const CONFIGURABLE = 0b0000_0100; + const GET = 0b0000_1000; + const SET = 0b0001_0000; + } +} + +impl SlotAttributes { + pub(crate) const fn is_accessor_descriptor(self) -> bool { + self.contains(Self::GET) || self.contains(Self::SET) + } + + pub(crate) const fn has_get(self) -> bool { + self.contains(Self::GET) + } + pub(crate) const fn has_set(self) -> bool { + self.contains(Self::SET) + } + + /// Check if slot type width matches, this can only happens, + /// if they are both accessors, or both data properties. + pub(crate) const fn width_match(self, other: Self) -> bool { + self.is_accessor_descriptor() == other.is_accessor_descriptor() + } + + /// Get the width of the slot. + pub(crate) fn width(self) -> u32 { + // accessor take 2 positions in the storage to accomodate for the `get` and `set` fields. + 1 + u32::from(self.is_accessor_descriptor()) + } +} + +/// Represents an [`u32`] index and it's slot attributes of an element in a object storage. +/// +/// Slots can have different width depending on its attributes, accessors properties have width `2`, +/// while data properties have width `1`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Slot { + pub(crate) index: SlotIndex, + pub(crate) attributes: SlotAttributes, +} + +impl Slot { + /// Get the width of the slot. + pub(crate) fn width(self) -> u32 { + self.attributes.width() + } + + /// Calculate next slot from previous one. + /// + /// This is needed because slots do not have the same width. + pub(crate) fn from_previous( + previous_slot: Option, + new_attributes: SlotAttributes, + ) -> Self { + // If there was no previous slot then return 0 as the index. + let Some(previous_slot) = previous_slot else { + return Self { + index: 0, + attributes: new_attributes, + } + }; + + Self { + index: previous_slot.index + previous_slot.width(), + attributes: new_attributes, + } + } +} diff --git a/boa_engine/src/object/shape/unique_shape.rs b/boa_engine/src/object/shape/unique_shape.rs new file mode 100644 index 0000000000..5e052fb02c --- /dev/null +++ b/boa_engine/src/object/shape/unique_shape.rs @@ -0,0 +1,241 @@ +use std::{cell::RefCell, fmt::Debug}; + +use boa_gc::{Finalize, Gc, GcRefCell, Trace}; + +use crate::property::PropertyKey; + +use super::{ + property_table::PropertyTableInner, shared_shape::TransitionKey, ChangeTransition, + ChangeTransitionAction, JsPrototype, Shape, Slot, +}; + +/// The internal representation of [`UniqueShape`]. +#[derive(Default, Debug, Trace, Finalize)] +struct Inner { + /// The property table that maps a [`PropertyKey`] to a slot in the objects storage. + // + // SAFETY: This is safe becasue nothing in this field needs tracing. + #[unsafe_ignore_trace] + property_table: RefCell, + + /// The prototype of the shape. + prototype: GcRefCell, +} + +/// Represents a [`Shape`] that is not shared with any other object. +/// +/// This is useful for objects that are inherently unique like, +/// the builtin object. +/// +/// Cloning this does a shallow clone. +#[derive(Default, Debug, Clone, Trace, Finalize)] +pub(crate) struct UniqueShape { + inner: Gc, +} + +impl UniqueShape { + /// Create a new [`UniqueShape`]. + pub(crate) fn new(prototype: JsPrototype, property_table: PropertyTableInner) -> Self { + Self { + inner: Gc::new(Inner { + property_table: RefCell::new(property_table), + prototype: GcRefCell::new(prototype), + }), + } + } + + pub(crate) fn override_internal( + &self, + property_table: PropertyTableInner, + prototype: JsPrototype, + ) { + *self.inner.property_table.borrow_mut() = property_table; + *self.inner.prototype.borrow_mut() = prototype; + } + + /// Get the prototype of the [`UniqueShape`]. + pub(crate) fn prototype(&self) -> JsPrototype { + self.inner.prototype.borrow().clone() + } + + /// Get the property table of the [`UniqueShape`]. + pub(crate) fn property_table(&self) -> &RefCell { + &self.inner.property_table + } + + /// Inserts a new property into the [`UniqueShape`]. + pub(crate) fn insert_property_transition(&self, key: TransitionKey) -> Self { + let mut property_table = self.property_table().borrow_mut(); + property_table.insert(key.property_key, key.attributes); + self.clone() + } + + /// Remove a property from the [`UniqueShape`]. + /// + /// This will cause the current shape to be invalidated, and a new [`UniqueShape`] will be returned. + pub(crate) fn remove_property_transition(&self, key: &PropertyKey) -> Self { + let mut property_table = self.property_table().borrow_mut(); + let Some((index, _attributes)) = property_table.map.remove(key) else { + return self.clone(); + }; + + let index = index as usize; + + // shift elements + property_table.keys.remove(index); + + // The property that was deleted was not the last property added. + // Therefore we need to create a new unique shape, + // to invalidate any pointers to this shape i.e inline caches. + let mut property_table = std::mem::take(&mut *property_table); + + // If it is not the last property that was deleted, + // then update all the property slots that are after it. + if index != property_table.keys.len() { + // Get the previous value before the value at index, + // + // NOTE: calling wrapping_sub when usize index is 0 will wrap into usize::MAX + // which will return None, avoiding unneeded checks. + let mut previous_slot = property_table.keys.get(index.wrapping_sub(1)).map(|x| x.1); + + // Update all slot positions + for (index, (key, slot)) in property_table.keys.iter_mut().enumerate().skip(index) { + *slot = Slot::from_previous(previous_slot, slot.attributes); + + let Some((map_index, map_slot)) = property_table.map.get_mut(key) else { + unreachable!("There should already be a property") + }; + *map_index = index as u32; + *map_slot = *slot; + + previous_slot = Some(*slot); + } + } + + let prototype = self.inner.prototype.borrow_mut().take(); + Self::new(prototype, property_table) + } + + /// Does a property lookup on the [`UniqueShape`] returning the [`Slot`] where it's + /// located or [`None`] otherwise. + pub(crate) fn lookup(&self, key: &PropertyKey) -> Option { + let property_table = self.property_table().borrow(); + if let Some((_, slot)) = property_table.map.get(key) { + return Some(*slot); + } + + None + } + + /// Change the attributes of a property from the [`UniqueShape`]. + /// + /// This will cause the current shape to be invalidated, and a new [`UniqueShape`] will be returned. + /// + /// NOTE: This assumes that the property had already been inserted. + pub(crate) fn change_attributes_transition( + &self, + key: &TransitionKey, + ) -> ChangeTransition { + let mut property_table = self.property_table().borrow_mut(); + let Some((index, slot)) = property_table.map.get_mut(&key.property_key) else { + unreachable!("Attribute change can only happen on existing property") + }; + + let index = *index as usize; + + // If property does not change type, there is no need to shift. + if slot.attributes.width_match(key.attributes) { + slot.attributes = key.attributes; + property_table.keys[index].1.attributes = key.attributes; + // TODO: invalidate the pointer. + return ChangeTransition { + shape: Shape::unique(self.clone()), + action: ChangeTransitionAction::Nothing, + }; + } + slot.attributes = key.attributes; + + let mut previous_slot = *slot; + + property_table.keys[index].1.attributes = key.attributes; + + let action = if key.attributes.is_accessor_descriptor() { + // Data --> Accessor + ChangeTransitionAction::Insert + } else { + // Accessor --> Data + ChangeTransitionAction::Remove + }; + + // The property that was deleted was not the last property added. + // Therefore we need to create a new unique shape, + // to invalidate any pointers to this shape i.e inline caches. + let mut property_table = std::mem::take(&mut *property_table); + + // Update all slot positions, after the target property. + // + // Example: Change 2nd one from accessor to data + // + // | Idx: 0, DATA | Idx: 1, ACCESSOR | Idx: 3, DATA | + // \----- target + // + // Change attributes of target (given trough arguments). + // + // | Idx: 0, DATA | Idx: 1, DATA | Idx: 3, DATA | + // \----- target + // + // The target becomes the previous pointer and next is target_index + 1, + // continue until we reach the end. + // + // | Idx: 0, DATA | Idx: 1, DATA | Idx: 3, DATA | + // previous ----/ \-------- next + // + // When next is out of range we quit, everything has been set. + // + // | Idx: 0, DATA | Idx: 1, DATA | Idx: 2, DATA | EMPTY | + // previous ----/ \-------- next + // + let next = index + 1; + for (key, slot) in property_table.keys.iter_mut().skip(next) { + *slot = Slot::from_previous(Some(previous_slot), slot.attributes); + + let Some((_, map_slot)) = property_table.map.get_mut(key) else { + unreachable!("There should already be a property") + }; + *map_slot = *slot; + + previous_slot = *slot; + } + + let prototype = self.inner.prototype.borrow_mut().take(); + let shape = Self::new(prototype, property_table); + + ChangeTransition { + shape: Shape::unique(shape), + action, + } + } + + /// Change the prototype of the [`UniqueShape`]. + /// + /// This will cause the current shape to be invalidated, and a new [`UniqueShape`] will be returned. + pub(crate) fn change_prototype_transition(&self, prototype: JsPrototype) -> Self { + let mut property_table = self.inner.property_table.borrow_mut(); + + // We need to create a new unique shape, + // to invalidate any pointers to this shape i.e inline caches. + let property_table = std::mem::take(&mut *property_table); + Self::new(prototype, property_table) + } + + /// Gets all keys first strings then symbols in creation order. + pub(crate) fn keys(&self) -> Vec { + self.property_table().borrow().keys() + } + + /// Return location in memory of the [`UniqueShape`]. + pub(crate) fn to_addr_usize(&self) -> usize { + let ptr: *const _ = self.inner.as_ref(); + ptr as usize + } +} diff --git a/boa_engine/src/property/attribute/mod.rs b/boa_engine/src/property/attribute/mod.rs index ac816d172a..fe67effba1 100644 --- a/boa_engine/src/property/attribute/mod.rs +++ b/boa_engine/src/property/attribute/mod.rs @@ -15,7 +15,7 @@ bitflags! { /// - `[[Configurable]]` (`CONFIGURABLE`) - If `false`, attempts to delete the property, /// change the property to be an `accessor property`, or change its attributes (other than `[[Value]]`, /// or changing `[[Writable]]` to `false`) will fail. - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Attribute: u8 { /// The `Writable` attribute decides whether the value associated with the property can be changed or not, from its initial value. const WRITABLE = 0b0000_0001; diff --git a/boa_engine/src/property/mod.rs b/boa_engine/src/property/mod.rs index 58601d4728..8adcd5a761 100644 --- a/boa_engine/src/property/mod.rs +++ b/boa_engine/src/property/mod.rs @@ -17,7 +17,7 @@ mod attribute; -use crate::{js_string, JsString, JsSymbol, JsValue}; +use crate::{js_string, object::shape::slot::SlotAttributes, JsString, JsSymbol, JsValue}; use boa_gc::{Finalize, Trace}; use std::{fmt, iter::FusedIterator}; @@ -338,6 +338,19 @@ impl PropertyDescriptor { self.configurable = Some(configurable); } } + + pub(crate) fn to_slot_attributes(&self) -> SlotAttributes { + let mut attributes = SlotAttributes::empty(); + attributes.set(SlotAttributes::CONFIGURABLE, self.expect_configurable()); + attributes.set(SlotAttributes::ENUMERABLE, self.expect_enumerable()); + if self.is_data_descriptor() { + attributes.set(SlotAttributes::WRITABLE, self.expect_writable()); + } else { + attributes.set(SlotAttributes::GET, self.get().is_some()); + attributes.set(SlotAttributes::SET, self.set().is_some()); + } + attributes + } } /// A builder for [`PropertyDescriptor`]. @@ -557,7 +570,7 @@ impl From for PropertyDescriptor { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-ispropertykey -#[derive(PartialEq, Debug, Clone, Eq, Hash)] +#[derive(Finalize, PartialEq, Debug, Clone, Eq, Hash)] pub enum PropertyKey { /// A string property key. String(JsString), diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index e98ad23c58..4a6f410481 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -9,7 +9,7 @@ use crate::{ context::{intrinsics::Intrinsics, HostHooks}, environments::DeclarativeEnvironment, - object::JsObject, + object::{shape::shared_shape::SharedShape, JsObject}, }; use boa_gc::{Finalize, Gc, Trace}; use boa_profiler::Profiler; @@ -39,10 +39,10 @@ struct Inner { impl Realm { /// Create a new Realm. #[inline] - pub fn create(hooks: &dyn HostHooks) -> Self { + pub fn create(hooks: &dyn HostHooks, root_shape: &SharedShape) -> Self { let _timer = Profiler::global().start_event("Realm::create", "realm"); - let intrinsics = Intrinsics::default(); + let intrinsics = Intrinsics::new(root_shape); let global_object = hooks.create_global_object(&intrinsics); let global_this = hooks .create_global_this(&intrinsics) diff --git a/boa_engine/src/value/conversions/serde_json.rs b/boa_engine/src/value/conversions/serde_json.rs index ae5c1e75df..b4131ee114 100644 --- a/boa_engine/src/value/conversions/serde_json.rs +++ b/boa_engine/src/value/conversions/serde_json.rs @@ -137,8 +137,8 @@ impl JsValue { Ok(Value::Array(arr)) } else { let mut map = Map::new(); - for (key, property) in obj.borrow().properties().iter() { - let key = match &key { + for property_key in obj.borrow().properties().shape.keys() { + let key = match &property_key { PropertyKey::String(string) => string.to_std_string_escaped(), PropertyKey::Index(i) => i.to_string(), PropertyKey::Symbol(_sym) => { @@ -148,7 +148,12 @@ impl JsValue { } }; - let value = match property.value() { + let value = match obj + .borrow() + .properties() + .get(&property_key) + .and_then(|x| x.value().cloned()) + { Some(val) => val.to_json(context)?, None => Value::Null, }; diff --git a/boa_engine/src/value/display.rs b/boa_engine/src/value/display.rs index 7050198852..385772202e 100644 --- a/boa_engine/src/value/display.rs +++ b/boa_engine/src/value/display.rs @@ -67,15 +67,19 @@ macro_rules! print_obj_value { } }; (props of $obj:expr, $display_fn:ident, $indent:expr, $encounters:expr, $print_internals:expr) => { - print_obj_value!(impl $obj, |(key, val)| { + {let mut keys: Vec<_> = $obj.borrow().properties().index_property_keys().map(crate::property::PropertyKey::Index).collect(); + keys.extend($obj.borrow().properties().shape.keys()); + let mut result = Vec::default(); + for key in keys { + let val = $obj.borrow().properties().get(&key).expect("There should be a value"); if val.is_data_descriptor() { let v = &val.expect_value(); - format!( + result.push(format!( "{:>width$}: {}", key, $display_fn(v, $encounters, $indent.wrapping_add(4), $print_internals), width = $indent, - ) + )); } else { let display = match (val.set().is_some(), val.get().is_some()) { (true, true) => "Getter & Setter", @@ -83,20 +87,10 @@ macro_rules! print_obj_value { (false, true) => "Getter", _ => "No Getter/Setter" }; - format!("{:>width$}: {}", key, display, width = $indent) + result.push(format!("{:>width$}: {}", key, display, width = $indent)); } - }) - }; - - // A private overload of the macro - // DO NOT use directly - (impl $v:expr, $f:expr) => { - $v - .borrow() - .properties() - .iter() - .map($f) - .collect::>() + } + result} }; } diff --git a/boa_engine/src/value/hash.rs b/boa_engine/src/value/hash.rs index 6536b1a77a..c657037b1c 100644 --- a/boa_engine/src/value/hash.rs +++ b/boa_engine/src/value/hash.rs @@ -45,7 +45,7 @@ impl Hash for JsValue { Self::BigInt(ref bigint) => bigint.hash(state), Self::Rational(rational) => RationalHashable(*rational).hash(state), Self::Symbol(ref symbol) => Hash::hash(symbol, state), - Self::Object(ref object) => std::ptr::hash(object.as_ref(), state), + Self::Object(ref object) => object.hash(state), } } } diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index c27dedd30f..f302f466b4 100644 --- a/boa_engine/src/value/mod.rs +++ b/boa_engine/src/value/mod.rs @@ -21,7 +21,6 @@ use crate::{ error::JsNativeError, object::{JsObject, ObjectData}, property::{PropertyDescriptor, PropertyKey}, - string::utf16, symbol::JsSymbol, Context, JsBigInt, JsResult, JsString, }; @@ -467,61 +466,40 @@ impl JsValue { /// /// See: pub fn to_object(&self, context: &mut Context<'_>) -> JsResult { + // TODO: add fast paths with object template match self { Self::Undefined | Self::Null => Err(JsNativeError::typ() .with_message("cannot convert 'null' or 'undefined' to object") .into()), - Self::Boolean(boolean) => { - let prototype = context.intrinsics().constructors().boolean().prototype(); - Ok(JsObject::from_proto_and_data( - prototype, - ObjectData::boolean(*boolean), - )) - } - Self::Integer(integer) => { - let prototype = context.intrinsics().constructors().number().prototype(); - Ok(JsObject::from_proto_and_data( - prototype, - ObjectData::number(f64::from(*integer)), - )) - } - Self::Rational(rational) => { - let prototype = context.intrinsics().constructors().number().prototype(); - Ok(JsObject::from_proto_and_data( - prototype, - ObjectData::number(*rational), - )) - } - Self::String(ref string) => { - let prototype = context.intrinsics().constructors().string().prototype(); - - let object = - JsObject::from_proto_and_data(prototype, ObjectData::string(string.clone())); - // Make sure the correct length is set on our new string object - object.insert_property( - utf16!("length"), - PropertyDescriptor::builder() - .value(string.len()) - .writable(false) - .enumerable(false) - .configurable(false), - ); - Ok(object) - } - Self::Symbol(ref symbol) => { - let prototype = context.intrinsics().constructors().symbol().prototype(); - Ok(JsObject::from_proto_and_data( - prototype, - ObjectData::symbol(symbol.clone()), - )) - } - Self::BigInt(ref bigint) => { - let prototype = context.intrinsics().constructors().bigint().prototype(); - Ok(JsObject::from_proto_and_data( - prototype, - ObjectData::big_int(bigint.clone()), - )) - } + Self::Boolean(boolean) => Ok(context + .intrinsics() + .templates() + .boolean() + .create(ObjectData::boolean(*boolean), Vec::default())), + Self::Integer(integer) => Ok(context + .intrinsics() + .templates() + .number() + .create(ObjectData::number(f64::from(*integer)), Vec::default())), + Self::Rational(rational) => Ok(context + .intrinsics() + .templates() + .number() + .create(ObjectData::number(*rational), Vec::default())), + Self::String(ref string) => Ok(context.intrinsics().templates().string().create( + ObjectData::string(string.clone()), + vec![string.len().into()], + )), + Self::Symbol(ref symbol) => Ok(context + .intrinsics() + .templates() + .symbol() + .create(ObjectData::symbol(symbol.clone()), Vec::default())), + Self::BigInt(ref bigint) => Ok(context + .intrinsics() + .templates() + .bigint() + .create(ObjectData::big_int(bigint.clone()), Vec::default())), Self::Object(jsobject) => Ok(jsobject.clone()), } } diff --git a/boa_engine/src/value/tests.rs b/boa_engine/src/value/tests.rs index b714661efb..21d7637eb3 100644 --- a/boa_engine/src/value/tests.rs +++ b/boa_engine/src/value/tests.rs @@ -1,3 +1,4 @@ +use boa_macros::utf16; use indoc::indoc; use super::*; diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index a33daa30e7..cf0594cb79 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -12,10 +12,7 @@ use crate::{ context::intrinsics::StandardConstructors, environments::{BindingLocator, CompileTimeEnvironment}, error::JsNativeError, - object::{ - internal_methods::get_prototype_from_constructor, JsObject, ObjectData, CONSTRUCTOR, - PROTOTYPE, - }, + object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData, PROTOTYPE}, property::PropertyDescriptor, realm::Realm, string::utf16, @@ -576,6 +573,14 @@ impl ToInternedString for CodeBlock { } /// Creates a new function object. +/// +/// This is used in cases that the prototype is not known if it's [`None`] or [`Some`]. +/// +/// If the prototype given is [`None`] it will use [`create_function_object_fast`]. Otherwise +/// it will construct the function from template objects that have all the fields except the +/// prototype, and will perform a prototype transition change to set the prototype. +/// +/// This is slower than direct object template construction that is done in [`create_function_object_fast`]. pub(crate) fn create_function_object( code: Gc, r#async: bool, @@ -584,38 +589,20 @@ pub(crate) fn create_function_object( method: bool, context: &mut Context<'_>, ) -> JsObject { - let _timer = Profiler::global().start_event("JsVmFunction::new", "vm"); + let _timer = Profiler::global().start_event("create_function_object", "vm"); - let function_prototype = if let Some(prototype) = prototype { - prototype - } else if r#async { - context - .intrinsics() - .constructors() - .async_function() - .prototype() - } else { - context.intrinsics().constructors().function().prototype() + let Some(prototype) = prototype else { + // fast path + return create_function_object_fast(code, r#async, arrow, method, context); }; - let name_property = PropertyDescriptor::builder() - .value( - context - .interner() - .resolve_expect(code.name) - .into_common::(false), - ) - .writable(false) - .enumerable(false) - .configurable(true) - .build(); + let name: JsValue = context + .interner() + .resolve_expect(code.name) + .into_common::(false) + .into(); - let length_property = PropertyDescriptor::builder() - .value(code.length) - .writable(false) - .enumerable(false) - .configurable(true) - .build(); + let length: JsValue = code.length.into(); let function = if r#async { Function::new( @@ -642,41 +629,114 @@ pub(crate) fn create_function_object( ) }; - let constructor = - JsObject::from_proto_and_data(function_prototype, ObjectData::function(function)); + let data = ObjectData::function(function); - let constructor_property = PropertyDescriptor::builder() - .value(constructor.clone()) - .writable(true) - .enumerable(false) - .configurable(true) - .build(); + let templates = context.intrinsics().templates(); - constructor - .define_property_or_throw(utf16!("length"), length_property, context) - .expect("failed to define the length property of the function"); - constructor - .define_property_or_throw(utf16!("name"), name_property, context) - .expect("failed to define the name property of the function"); + let (mut template, storage, constructor_prototype) = if r#async || arrow || method { + ( + templates.function_without_proto().clone(), + vec![length, name], + None, + ) + } else { + let constructor_prototype = templates + .function_prototype() + .create(ObjectData::ordinary(), vec![JsValue::undefined()]); - if !r#async && !arrow && !method { - let prototype = JsObject::with_object_proto(context.intrinsics()); - prototype - .define_property_or_throw(CONSTRUCTOR, constructor_property, context) - .expect("failed to define the constructor property of the function"); - - let prototype_property = PropertyDescriptor::builder() - .value(prototype) - .writable(true) - .enumerable(false) - .configurable(false) - .build(); - constructor - .define_property_or_throw(PROTOTYPE, prototype_property, context) - .expect("failed to define the prototype property of the function"); + let template = templates.function_with_prototype_without_proto(); + + ( + template.clone(), + vec![length, name, constructor_prototype.clone().into()], + Some(constructor_prototype), + ) + }; + + template.set_prototype(prototype); + + let contructor = template.create(data, storage); + + if let Some(constructor_prototype) = &constructor_prototype { + constructor_prototype.borrow_mut().properties_mut().storage[0] = contructor.clone().into(); } + contructor +} - constructor +/// Creates a new function object. +/// +/// This is prefered over [`create_function_object`] if prototype is [`None`], +/// because it constructs the funtion from a pre-initialized object template, +/// with all the properties and prototype set. +pub(crate) fn create_function_object_fast( + code: Gc, + r#async: bool, + arrow: bool, + method: bool, + context: &mut Context<'_>, +) -> JsObject { + let _timer = Profiler::global().start_event("create_function_object_fast", "vm"); + + let name: JsValue = context + .interner() + .resolve_expect(code.name) + .into_common::(false) + .into(); + + let length: JsValue = code.length.into(); + + let function = if r#async { + FunctionKind::Async { + code, + environments: context.vm.environments.clone(), + home_object: None, + class_object: None, + } + } else { + FunctionKind::Ordinary { + code, + environments: context.vm.environments.clone(), + constructor_kind: ConstructorKind::Base, + home_object: None, + fields: ThinVec::new(), + private_methods: ThinVec::new(), + class_object: None, + } + }; + + let function = Function::new(function, context.realm().clone()); + + let data = ObjectData::function(function); + + if r#async { + context + .intrinsics() + .templates() + .async_function() + .create(data, vec![length, name]) + } else if arrow || method { + context + .intrinsics() + .templates() + .function() + .create(data, vec![length, name]) + } else { + let prototype = context + .intrinsics() + .templates() + .function_prototype() + .create(ObjectData::ordinary(), vec![JsValue::undefined()]); + + let constructor = context + .intrinsics() + .templates() + .function_with_prototype() + .create(data, vec![length, name, prototype.clone().into()]); + + prototype.borrow_mut().properties_mut().storage[0] = constructor.clone().into(); + + constructor + } } /// Creates a new generator function object. @@ -722,7 +782,8 @@ pub(crate) fn create_generator_function_object( .configurable(true) .build(); - let prototype = JsObject::from_proto_and_data( + let prototype = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), if r#async { context.intrinsics().objects().async_generator() } else { @@ -741,7 +802,8 @@ pub(crate) fn create_generator_function_object( }, context.realm().clone(), ); - JsObject::from_proto_and_data( + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), function_prototype, ObjectData::async_generator_function(function), ) @@ -755,7 +817,11 @@ pub(crate) fn create_generator_function_object( }, context.realm().clone(), ); - JsObject::from_proto_and_data(function_prototype, ObjectData::generator_function(function)) + JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + function_prototype, + ObjectData::generator_function(function), + ) }; let prototype_property = PropertyDescriptor::builder() @@ -1086,7 +1152,8 @@ impl JsObject { }) }; - let generator = Self::from_proto_and_data(proto, data); + let generator = + Self::from_proto_and_data_with_shared_shape(context.root_shape(), proto, data); if async_ { let gen_clone = generator.clone(); @@ -1146,7 +1213,11 @@ impl JsObject { StandardConstructors::object, context, )?; - Ok(Self::from_proto_and_data(prototype, ObjectData::ordinary())) + Ok(Self::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::ordinary(), + )) } else { Err(JsNativeError::typ() .with_message( @@ -1178,7 +1249,11 @@ impl JsObject { StandardConstructors::object, context, )?; - let this = Self::from_proto_and_data(prototype, ObjectData::ordinary()); + let this = Self::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::ordinary(), + ); this.initialize_instance_elements(self, context)?; diff --git a/boa_engine/src/vm/opcode/get/function.rs b/boa_engine/src/vm/opcode/get/function.rs index 83cc8b92a1..dd32a0d04e 100644 --- a/boa_engine/src/vm/opcode/get/function.rs +++ b/boa_engine/src/vm/opcode/get/function.rs @@ -1,5 +1,5 @@ use crate::{ - vm::{code_block::create_function_object, opcode::Operation, CompletionType}, + vm::{code_block::create_function_object_fast, opcode::Operation, CompletionType}, Context, JsResult, }; @@ -18,7 +18,7 @@ impl Operation for GetArrowFunction { let index = context.vm.read::(); context.vm.read::(); let code = context.vm.frame().code_block.functions[index as usize].clone(); - let function = create_function_object(code, false, true, None, false, context); + let function = create_function_object_fast(code, false, true, false, context); context.vm.push(function); Ok(CompletionType::Normal) } @@ -39,7 +39,7 @@ impl Operation for GetAsyncArrowFunction { let index = context.vm.read::(); context.vm.read::(); let code = context.vm.frame().code_block.functions[index as usize].clone(); - let function = create_function_object(code, true, true, None, false, context); + let function = create_function_object_fast(code, true, true, false, context); context.vm.push(function); Ok(CompletionType::Normal) } @@ -60,7 +60,7 @@ impl Operation for GetFunction { let index = context.vm.read::(); let method = context.vm.read::() != 0; let code = context.vm.frame().code_block.functions[index as usize].clone(); - let function = create_function_object(code, false, false, None, method, context); + let function = create_function_object_fast(code, false, false, method, context); context.vm.push(function); Ok(CompletionType::Normal) } @@ -81,7 +81,7 @@ impl Operation for GetFunctionAsync { let index = context.vm.read::(); let method = context.vm.read::() != 0; let code = context.vm.frame().code_block.functions[index as usize].clone(); - let function = create_function_object(code, true, false, None, method, context); + let function = create_function_object_fast(code, true, false, method, context); context.vm.push(function); Ok(CompletionType::Normal) } diff --git a/boa_engine/src/vm/opcode/push/array.rs b/boa_engine/src/vm/opcode/push/array.rs index 18e5fa3e93..db5b40334b 100644 --- a/boa_engine/src/vm/opcode/push/array.rs +++ b/boa_engine/src/vm/opcode/push/array.rs @@ -1,8 +1,9 @@ use crate::{ builtins::{iterable::IteratorRecord, Array}, + object::ObjectData, string::utf16, vm::{opcode::Operation, CompletionType}, - Context, JsResult, + Context, JsResult, JsValue, }; /// `PushNewArray` implements the Opcode Operation for `Opcode::PushNewArray` @@ -17,8 +18,11 @@ impl Operation for PushNewArray { const INSTRUCTION: &'static str = "INST - PushNewArray"; fn execute(context: &mut Context<'_>) -> JsResult { - let array = Array::array_create(0, None, context) - .expect("Array creation with 0 length should never fail"); + let array = context + .intrinsics() + .templates() + .array() + .create(ObjectData::array(), vec![JsValue::new(0)]); context.vm.push(array); Ok(CompletionType::Normal) } diff --git a/boa_engine/src/vm/opcode/push/object.rs b/boa_engine/src/vm/opcode/push/object.rs index b7681dee1d..46089038ed 100644 --- a/boa_engine/src/vm/opcode/push/object.rs +++ b/boa_engine/src/vm/opcode/push/object.rs @@ -1,5 +1,5 @@ use crate::{ - object::JsObject, + object::ObjectData, vm::{opcode::Operation, CompletionType}, Context, JsResult, }; @@ -16,7 +16,11 @@ impl Operation for PushEmptyObject { const INSTRUCTION: &'static str = "INST - PushEmptyObject"; fn execute(context: &mut Context<'_>) -> JsResult { - let o = JsObject::with_object_proto(context.intrinsics()); + let o = context + .intrinsics() + .templates() + .ordinary_object() + .create(ObjectData::ordinary(), Vec::default()); context.vm.push(o); Ok(CompletionType::Normal) } diff --git a/boa_engine/src/vm/opcode/set/class_prototype.rs b/boa_engine/src/vm/opcode/set/class_prototype.rs index b1ece9f5eb..d647e7e8ae 100644 --- a/boa_engine/src/vm/opcode/set/class_prototype.rs +++ b/boa_engine/src/vm/opcode/set/class_prototype.rs @@ -25,7 +25,11 @@ impl Operation for SetClassPrototype { _ => unreachable!(), }; - let proto = JsObject::from_proto_and_data(prototype, ObjectData::ordinary()); + let proto = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::ordinary(), + ); let class = context.vm.pop(); { diff --git a/boa_engine/src/vm/opcode/set/property.rs b/boa_engine/src/vm/opcode/set/property.rs index 3339457b87..c330735888 100644 --- a/boa_engine/src/vm/opcode/set/property.rs +++ b/boa_engine/src/vm/opcode/set/property.rs @@ -78,7 +78,7 @@ impl Operation for SetPropertyByValue { break 'fast_path; } - let prototype = object_borrowed.prototype().clone(); + let shape = object_borrowed.shape().clone(); if let Some(dense_elements) = object_borrowed .properties_mut() @@ -93,6 +93,7 @@ impl Operation for SetPropertyByValue { // Cannot use fast path if the [[prototype]] is a proxy object, // because we have to the call prototypes [[set]] on non-existing property, // and proxy objects can override [[set]]. + let prototype = shape.prototype(); if prototype.map_or(false, |x| x.is_proxy()) { break 'fast_path; } diff --git a/docs/boa_object.md b/docs/boa_object.md index 4f2fea040e..fb8fc80da5 100644 --- a/docs/boa_object.md +++ b/docs/boa_object.md @@ -201,3 +201,42 @@ let global = $boa.realm.create(); Object != global.Object; // true ``` + +## Module `$boa.shape` + +This module contains helpful functions for getting information about a shape of an object. + +### Function `$boa.shape.id(object)` + +Returns the pointer of the object's shape in memory as a string encoded in hexadecimal format. + +```JavaScript +$boa.shape.id(Number) // '0x7FC35A073868' +$boa.shape.id({}) // '0x7FC35A046258' +``` + +### Function `$boa.shape.type(object)` + +Returns the object's shape type. + +```JavaScript +$boa.shape.type({x: 3}) // 'shared' +$boa.shape.type(Number) // 'unique' +``` + +### Function `$boa.shape.same(o1, o2)` + +Returns `true` if both objects have the same shape. + +```JavaScript +// The values of the properties are not important! +let o1 = { x: 10 } +let o2 = {} +$boa.shape.same(o1, o2) // false + +o2.x = 20 +$boa.shape.same(o1, o2) // true + +o2.y = 200 +$boa.shape.same(o1, o2) // false +``` diff --git a/docs/shapes.md b/docs/shapes.md new file mode 100644 index 0000000000..aa00444cb7 --- /dev/null +++ b/docs/shapes.md @@ -0,0 +1,220 @@ +# Shapes (Hidden Classes) + +The best way to explain object shapes is through examples. It all begins with the root shape. + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + + Root(Root Shape\nPrototype: None) -->| Property Count 0 | PropertyTable(PropertyTable\n) +``` + +The root shape is where the transition chain starts from, it has a pointer to a `PropertyTable`, +we will explain what it is and does later on! + +**NOTE:** We will annotate the shapes with `S` followed by a number. + +If we have an example of JavaScript code like: + +```js +let o = {}; +``` + +The following chain is created: + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + + Root(S0: Root Shape\nPrototype: None) -->|Property Count: 0| PropertyTable(PropertyTable\n) + + ObjectPrototype(S1: Prototype Shape\nPrototype: Object.prototype) -->|Property Count: 0|PropertyTable + ObjectPrototype:::New -->|Previous| Root +``` + +We transition, the object `o` has `S1` shape. The root shape does not have a prototype. So we transition into a shape that has the +`Object.prototype` as `__proto__`. We can see that the shapes inherited the `PropertyTable` from the `root`. + +Ok, Let us add a property `x`: + +```js +o.x = 100; // The value is not important! +``` + +Then this happens: + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + + Root(S0: Root Shape\nPrototype: None) -->|Property Count: 0| PropertyTable(PropertyTable\nx: Slot 0, writable, configurable, enumerable) + + ObjectPrototype(S1: Prototype Shape\nPrototype: Object.prototype) -->|Property Count: 0|PropertyTable + ObjectPrototype -->|Previous| Root + + InsertX(S2: Insert Shape\nProperty: 'x') --> |Property Count: 1|PropertyTable + InsertX:::New -->|Previous| ObjectPrototype +``` + +The object `o` has shape `S2` shape now, we can see that it also inherited the `PropertyTable`, but it's property count is `1` and +an entry has been added into the `PropertyTable`. + +We can see that the property added is `writable`, `configurable`, and `enumerable`, but we also see `Slot 0`, +this is the index into the dense storage in the object itself. + +Here is how it would look with the `o` object: + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + + Root(S0: Root Shape\nPrototype: None) -->| Property Count: 0 | PropertyTable(PropertyTable\nx: Slot 0, writable, configurable, enumerable) + + ObjectPrototype(S1: Prototype Shape\nPrototype: Object.prototype) -->| Property Count: 0 |PropertyTable + ObjectPrototype -->|Previous| Root + + InsertX(S2: Insert Shape\nProperty: 'x') --> | Property Count: 1 | PropertyTable + InsertX -->|Previous| ObjectPrototype + + O(Object o\nElement 0: JsValue: 100) + O:::New --> InsertX +``` + +Let's define a getter and setter `y` + +```js +// What the getter/setter are not important! +Object.defineProperty(o, "y", { + enumerable: true, + configurable: true, + get: function () { + return this.x; + }, + set: function (value) { + this.x = value; + }, +}); +``` + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + + Root(S0: Root Shape\nPrototype: None) -->|Property Count: 0| PropertyTable(PropertyTable\nx: Slot 0, writable, configurable, enumerable\ny: Slot 1, has_get, has_set, configurable, enumerable) + + ObjectPrototype(S1: Prototype Shape\nPrototype: Object.prototype) -->| Property Count: 0 |PropertyTable + ObjectPrototype -->|Previous| Root + + InsertX(S2: Insert Shape\nProperty: 'x') --> |Property Count: 1| PropertyTable + InsertX -->|Previous| ObjectPrototype + + InsertY(S3: Insert Shape\nProperty: 'y') --> |Property Count: 2| PropertyTable + InsertY:::New -->|Previous| InsertX + + O(Object o\nElement 0: JsValue: 100\nElement 1: JsValue: func\nElement 2: JsValue: func) --> InsertY +``` + +We can see that the property has been added into the property table, it has the `has_get` and `has_set` flags set, +in the object there are two elements added, the first is the `get` function and the second is the `set` function. + +Slots are varying in length, two for accessor properties and one for data properties, the index points to the first +value in the object storage. + +What would happen if an object had `S2` shape and we tried to access a property `y` how does it know if it +has or doesn't have a property named `y`? By the property count on the shape, it has property count `1`, +all the object in the `PropertyTable` are stored in a map that preserves the order and and can be indexed. + +When we do a lookup the on property table, if the index of the property is greater than the property count (`1`), +than it does not belong to the shape. + +Now, Let's create a new object `o2`, with property `x`: + +```js +let o2 = { x: 200 }; +``` + +After this `o2` would have the `S2` shape. + +How does the shape know that it can reuse `S1` then to go to `S2`? This is not the real structure! +Every shape has pointers to forward transitions that happened, these are weak pointers so we don't keep +alive unused shapes. The pointers have been omitted, so the diagrams are clearer (too many arrows). + +Ok, now let us define a property `z` instead of `y`: + +```js +o2.z = 300; +``` + +The following changes accure to the shape tree: + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + style PropertyTableFork fill:#071E22 + + Root(S0: Root Shape\nPrototype: None) -->| Property Count: 0 | PropertyTable(PropertyTable\nx: Slot 0, writable, configurable, enumerable\ny: Slot 1, has_get, has_set, configurable, enumerable) + + ObjectPrototype(S1: Prototype Shape\nPrototype: Object.prototype) -->| Property Count: 0 |PropertyTable + ObjectPrototype -->|Previous| Root + + InsertX(S2: Insert Shape\nProperty: 'x') --> | Property Count: 1 | PropertyTable + InsertX -->|Previous| ObjectPrototype + + InsertY(S3: Insert Shape\nProperty: 'y') --> | Property Count: 2 | PropertyTable + InsertY -->|Previous| InsertX + + PropertyTableFork(PropertyTable\nx: Slot 0, writable, configurable, enumerable\nz: Slot 1, writable, configurable, enumerable) + InsertZ(S4: Insert Shape\nProperty: 'z') --> | Property Count: 2 | PropertyTableFork:::New + InsertZ:::New -->|Previous| InsertX + + O(Object o\nElement 0: JsValue: 100\nElement 1: JsValue: func\nElement 2: JsValue: func) --> InsertY + O2(Object o2\nElement 0: JsValue: 200\nElement 1: JsValue: 300) + O2:::New --> InsertZ +``` + +Now `o2` has `S4` shape. We can also see that `PropertyTable` has been forked, because we can no longer add a property at position `1`. + +What would happen if we wanted to delete a property `x` from object `o`: + +```js +delete o.x; +``` + +```mermaid +flowchart LR + classDef New style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + style Root stroke:#000,stroke-width:5px + style PropertyTable fill:#071E22 + style PropertyTableFork fill:#071E22 + + Root(S0: Root Shape\nPrototype: None) -->| Property Count: 0 | PropertyTable(PropertyTable\nx: Slot 0, writable, configurable, enumerable\ny: Slot 1, has_get, has_set, configurable, enumerable) + + ObjectPrototype(S1: Prototype Shape\nPrototype: Object.prototype) -->| Property Count: 0 |PropertyTable + ObjectPrototype -->|Previous| Root + + + PropertyTableFork(PropertyTable\ny: Slot 0, has_get, has_set, configurable, enumerable) + InsertYNew(S3: Insert Shape\nProperty: y) --> | Property Count: 1 |PropertyTableFork:::New + InsertYNew:::New -->|Previous| ObjectPrototype + + O(Object o\nElement 0: JsValue: func\nElement 1: JsValue: func) --> InsertYNew + O:::New +``` + +**NOTE:**: `o2` and its shape have been omitted from the diagram. + +When a deletion happens, we find the node in the chain where we added the property, and get it's parent (`base`), +we also remember that what transitions happened after the property insertion, then we apply them +one by one until we construct the chain and return the last shape in that chain.