diff --git a/boa_cli/src/debug/mod.rs b/boa_cli/src/debug/mod.rs index 48a02a0bd28..050c29fce44 100644 --- a/boa_cli/src/debug/mod.rs +++ b/boa_cli/src/debug/mod.rs @@ -7,10 +7,12 @@ mod function; mod gc; mod object; mod optimizer; +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); @@ -25,6 +27,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 00000000000..167bc5b3c47 --- /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 a5591d63abe..5317bbdb650 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 c625478858c..6f0c103f666 100644 --- a/boa_engine/src/builtins/array/array_iterator.rs +++ b/boa_engine/src/builtins/array/array_iterator.rs @@ -77,7 +77,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 793d4d8d72b..096759fb4bd 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -46,20 +46,21 @@ impl IntrinsicObject for Array { let symbol_iterator = JsSymbol::iterator(); let symbol_unscopables = JsSymbol::unscopables(); - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); - let values_function = - BuiltInBuilder::with_object(intrinsics, intrinsics.objects().array_prototype_values()) - .callable(Self::values) - .name("values") - .build(); + let values_function = BuiltInBuilder::callable_with_object( + intrinsics, + intrinsics.objects().array_prototype_values(), + Self::values, + ) + .name("values") + .build(); let unscopables_object = Self::unscopables_object(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_accessor( JsSymbol::species(), Some(get_species), @@ -203,17 +204,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()) @@ -239,6 +241,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]] »). @@ -246,7 +258,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( @@ -276,27 +307,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 be5b99fedfb..e095a3bfc53 100644 --- a/boa_engine/src/builtins/array_buffer/mod.rs +++ b/boa_engine/src/builtins/array_buffer/mod.rs @@ -52,17 +52,15 @@ impl IntrinsicObject for ArrayBuffer { let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); - let get_byte_length = BuiltInBuilder::new(intrinsics) - .callable(Self::get_byte_length) + let get_byte_length = BuiltInBuilder::callable(intrinsics, Self::get_byte_length) .name("get byteLength") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .accessor( utf16!("byteLength"), Some(get_byte_length), @@ -354,7 +352,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/async_function/mod.rs b/boa_engine/src/builtins/async_function/mod.rs index 53b0f0a89a8..1ac0acfd7ca 100644 --- a/boa_engine/src/builtins/async_function/mod.rs +++ b/boa_engine/src/builtins/async_function/mod.rs @@ -26,7 +26,7 @@ impl IntrinsicObject for AsyncFunction { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().function().constructor()) .inherits(Some(intrinsics.constructors().function().prototype())) .property( diff --git a/boa_engine/src/builtins/async_generator_function/mod.rs b/boa_engine/src/builtins/async_generator_function/mod.rs index e4d50e088ac..1e2d649273b 100644 --- a/boa_engine/src/builtins/async_generator_function/mod.rs +++ b/boa_engine/src/builtins/async_generator_function/mod.rs @@ -26,7 +26,7 @@ impl IntrinsicObject for AsyncGeneratorFunction { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .inherits(Some(intrinsics.constructors().function().prototype())) .constructor_attributes(Attribute::CONFIGURABLE) .property( diff --git a/boa_engine/src/builtins/bigint/mod.rs b/boa_engine/src/builtins/bigint/mod.rs index 2d2ac22aec4..4376262df0a 100644 --- a/boa_engine/src/builtins/bigint/mod.rs +++ b/boa_engine/src/builtins/bigint/mod.rs @@ -38,7 +38,7 @@ impl IntrinsicObject for BigInt { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .method(Self::to_string, "toString", 0) .method(Self::value_of, "valueOf", 0) .static_method(Self::as_int_n, "asIntN", 2) diff --git a/boa_engine/src/builtins/boolean/mod.rs b/boa_engine/src/builtins/boolean/mod.rs index 18cdea1750d..b160f09cf4a 100644 --- a/boa_engine/src/builtins/boolean/mod.rs +++ b/boa_engine/src/builtins/boolean/mod.rs @@ -31,7 +31,7 @@ impl IntrinsicObject for Boolean { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .method(Self::to_string, "toString", 0) .method(Self::value_of, "valueOf", 0) .build(); @@ -67,7 +67,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 0999c5a1c9b..199129b9e04 100644 --- a/boa_engine/src/builtins/dataview/mod.rs +++ b/boa_engine/src/builtins/dataview/mod.rs @@ -34,22 +34,19 @@ impl IntrinsicObject for DataView { fn init(intrinsics: &Intrinsics) { let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; - let get_buffer = BuiltInBuilder::new(intrinsics) - .callable(Self::get_buffer) + let get_buffer = BuiltInBuilder::callable(intrinsics, Self::get_buffer) .name("get buffer") .build(); - let get_byte_length = BuiltInBuilder::new(intrinsics) - .callable(Self::get_byte_length) + let get_byte_length = BuiltInBuilder::callable(intrinsics, Self::get_byte_length) .name("get byteLength") .build(); - let get_byte_offset = BuiltInBuilder::new(intrinsics) - .callable(Self::get_byte_offset) + let get_byte_offset = BuiltInBuilder::callable(intrinsics, Self::get_byte_offset) .name("get byteOffset") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .accessor(utf16!("buffer"), Some(get_buffer), None, flag_attributes) .accessor( utf16!("byteLength"), @@ -193,7 +190,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 aed1cbce60f..c2230f0c0da 100644 --- a/boa_engine/src/builtins/date/mod.rs +++ b/boa_engine/src/builtins/date/mod.rs @@ -103,7 +103,7 @@ impl IntrinsicObject for Date { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .method(Self::get_date::, "getDate", 0) .method(Self::get_day::, "getDay", 0) .method(Self::get_full_year::, "getFullYear", 0) @@ -267,7 +267,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 becc22f0154..3c6aee22ce7 100644 --- a/boa_engine/src/builtins/error/aggregate.rs +++ b/boa_engine/src/builtins/error/aggregate.rs @@ -30,7 +30,7 @@ impl IntrinsicObject for AggregateError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -66,7 +66,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 cd1581609d7..ca998b71d93 100644 --- a/boa_engine/src/builtins/error/eval.rs +++ b/boa_engine/src/builtins/error/eval.rs @@ -32,7 +32,7 @@ impl IntrinsicObject for EvalError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -65,7 +65,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 6c11588e384..7a9247725ea 100644 --- a/boa_engine/src/builtins/error/mod.rs +++ b/boa_engine/src/builtins/error/mod.rs @@ -131,7 +131,7 @@ impl IntrinsicObject for Error { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .property(utf16!("name"), Self::NAME, attribute) .property(utf16!("message"), "", attribute) .method(Self::to_string, "toString", 0) @@ -166,7 +166,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 e6618cf37be..7fe9f90b52e 100644 --- a/boa_engine/src/builtins/error/range.rs +++ b/boa_engine/src/builtins/error/range.rs @@ -30,7 +30,7 @@ impl IntrinsicObject for RangeError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -63,7 +63,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 710983db048..7f1a3042f7c 100644 --- a/boa_engine/src/builtins/error/reference.rs +++ b/boa_engine/src/builtins/error/reference.rs @@ -29,7 +29,7 @@ impl IntrinsicObject for ReferenceError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -65,7 +65,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 70811c4fcae..450af40c9fb 100644 --- a/boa_engine/src/builtins/error/syntax.rs +++ b/boa_engine/src/builtins/error/syntax.rs @@ -32,7 +32,7 @@ impl IntrinsicObject for SyntaxError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -68,7 +68,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 6a82717bf24..af53750e915 100644 --- a/boa_engine/src/builtins/error/type.rs +++ b/boa_engine/src/builtins/error/type.rs @@ -40,7 +40,7 @@ impl IntrinsicObject for TypeError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -73,7 +73,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 180a368d35f..f1a5a583b7e 100644 --- a/boa_engine/src/builtins/error/uri.rs +++ b/boa_engine/src/builtins/error/uri.rs @@ -31,7 +31,7 @@ impl IntrinsicObject for UriError { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().error().constructor()) .inherits(Some(intrinsics.constructors().error().prototype())) .property(utf16!("name"), Self::NAME, attribute) @@ -64,7 +64,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 df5d1fdb6c4..92fcf441e27 100644 --- a/boa_engine/src/builtins/escape/mod.rs +++ b/boa_engine/src/builtins/escape/mod.rs @@ -22,8 +22,7 @@ pub(crate) struct Escape; impl IntrinsicObject for Escape { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(escape) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, escape) .name(Self::NAME) .length(1) .build(); @@ -95,8 +94,7 @@ pub(crate) struct Unescape; impl IntrinsicObject for Unescape { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(unescape) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, 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 981efaf896a..544e611d3b9 100644 --- a/boa_engine/src/builtins/eval/mod.rs +++ b/boa_engine/src/builtins/eval/mod.rs @@ -28,8 +28,7 @@ impl IntrinsicObject for Eval { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(Self::eval) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, 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 40b4c97acd2..b882a5715fa 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 ca45f2206ea..ceed19891b7 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -401,21 +401,14 @@ impl IntrinsicObject for BuiltInFunctionObject { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event("function", "init"); - BuiltInBuilder::with_object(intrinsics, intrinsics.constructors().function().prototype()) - .callable(Self::prototype) - .name("") - .length(0) - .build(); - - let has_instance = BuiltInBuilder::new(intrinsics) - .callable(Self::has_instance) + let has_instance = BuiltInBuilder::callable(intrinsics, Self::has_instance) .name("[Symbol.iterator]") .length(1) .build(); let throw_type_error = intrinsics.objects().throw_type_error(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .method(Self::apply, "apply", 2) .method(Self::bind, "bind", 1) .method(Self::call, "call", 1) @@ -434,6 +427,15 @@ impl IntrinsicObject for BuiltInFunctionObject { Attribute::CONFIGURABLE, ) .build(); + + let prototype = intrinsics.constructors().function().prototype(); + + BuiltInBuilder::callable_with_object(intrinsics, prototype.clone(), Self::prototype) + .name("") + .length(0) + .build(); + + prototype.set_prototype(Some(intrinsics.constructors().object().prototype())); } fn get(intrinsics: &Intrinsics) -> JsObject { diff --git a/boa_engine/src/builtins/generator_function/mod.rs b/boa_engine/src/builtins/generator_function/mod.rs index 15d48636b3b..3c18c544038 100644 --- a/boa_engine/src/builtins/generator_function/mod.rs +++ b/boa_engine/src/builtins/generator_function/mod.rs @@ -31,7 +31,7 @@ impl IntrinsicObject for GeneratorFunction { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .inherits(Some(intrinsics.constructors().function().prototype())) .constructor_attributes(Attribute::CONFIGURABLE) .property( diff --git a/boa_engine/src/builtins/intl/collator/mod.rs b/boa_engine/src/builtins/intl/collator/mod.rs index 5af7149d1bb..7c70ff8b3d9 100644 --- a/boa_engine/src/builtins/intl/collator/mod.rs +++ b/boa_engine/src/builtins/intl/collator/mod.rs @@ -162,12 +162,11 @@ impl IntrinsicObject for Collator { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let compare = BuiltInBuilder::new(intrinsics) - .callable(Self::compare) + let compare = BuiltInBuilder::callable(intrinsics, Self::compare) .name("get compare") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_method(Self::supported_locales_of, "supportedLocalesOf", 1) .property( JsSymbol::to_string_tag(), @@ -353,7 +352,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, diff --git a/boa_engine/src/builtins/intl/date_time_format.rs b/boa_engine/src/builtins/intl/date_time_format.rs index ca38623e9ac..3f2552709e2 100644 --- a/boa_engine/src/builtins/intl/date_time_format.rs +++ b/boa_engine/src/builtins/intl/date_time_format.rs @@ -65,7 +65,7 @@ impl IntrinsicObject for DateTimeFormat { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics).build(); + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics).build(); } fn get(intrinsics: &Intrinsics) -> JsObject { diff --git a/boa_engine/src/builtins/intl/list_format/mod.rs b/boa_engine/src/builtins/intl/list_format/mod.rs index af3575c49ee..23dbd9a5bcb 100644 --- a/boa_engine/src/builtins/intl/list_format/mod.rs +++ b/boa_engine/src/builtins/intl/list_format/mod.rs @@ -51,7 +51,7 @@ impl IntrinsicObject for ListFormat { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_method(Self::supported_locales_of, "supportedLocalesOf", 1) .property( JsSymbol::to_string_tag(), diff --git a/boa_engine/src/builtins/intl/locale/mod.rs b/boa_engine/src/builtins/intl/locale/mod.rs index 264335c0ac9..6b3c92f2824 100644 --- a/boa_engine/src/builtins/intl/locale/mod.rs +++ b/boa_engine/src/builtins/intl/locale/mod.rs @@ -35,57 +35,47 @@ impl IntrinsicObject for Locale { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let base_name = BuiltInBuilder::new(intrinsics) - .callable(Self::base_name) + let base_name = BuiltInBuilder::callable(intrinsics, Self::base_name) .name("get baseName") .build(); - let calendar = BuiltInBuilder::new(intrinsics) - .callable(Self::calendar) + let calendar = BuiltInBuilder::callable(intrinsics, Self::calendar) .name("get calendar") .build(); - let case_first = BuiltInBuilder::new(intrinsics) - .callable(Self::case_first) + let case_first = BuiltInBuilder::callable(intrinsics, Self::case_first) .name("get caseFirst") .build(); - let collation = BuiltInBuilder::new(intrinsics) - .callable(Self::collation) + let collation = BuiltInBuilder::callable(intrinsics, Self::collation) .name("get collation") .build(); - let hour_cycle = BuiltInBuilder::new(intrinsics) - .callable(Self::hour_cycle) + let hour_cycle = BuiltInBuilder::callable(intrinsics, Self::hour_cycle) .name("get hourCycle") .build(); - let numeric = BuiltInBuilder::new(intrinsics) - .callable(Self::numeric) + let numeric = BuiltInBuilder::callable(intrinsics, Self::numeric) .name("get numeric") .build(); - let numbering_system = BuiltInBuilder::new(intrinsics) - .callable(Self::numbering_system) + let numbering_system = BuiltInBuilder::callable(intrinsics, Self::numbering_system) .name("get numberingSystem") .build(); - let language = BuiltInBuilder::new(intrinsics) - .callable(Self::language) + let language = BuiltInBuilder::callable(intrinsics, Self::language) .name("get language") .build(); - let script = BuiltInBuilder::new(intrinsics) - .callable(Self::script) + let script = BuiltInBuilder::callable(intrinsics, Self::script) .name("get script") .build(); - let region = BuiltInBuilder::new(intrinsics) - .callable(Self::region) + let region = BuiltInBuilder::callable(intrinsics, Self::region) .name("get region") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .property( JsSymbol::to_string_tag(), "Intl.Locale", diff --git a/boa_engine/src/builtins/intl/segmenter/mod.rs b/boa_engine/src/builtins/intl/segmenter/mod.rs index 6684bc4d9f8..6b74ce11f94 100644 --- a/boa_engine/src/builtins/intl/segmenter/mod.rs +++ b/boa_engine/src/builtins/intl/segmenter/mod.rs @@ -20,7 +20,7 @@ impl IntrinsicObject for Segmenter { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics).build(); + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics).build(); } fn get(intrinsics: &Intrinsics) -> JsObject { diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index c7e23bc4a5b..b393669fa79 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -4,7 +4,7 @@ use crate::{ builtins::{BuiltInBuilder, IntrinsicObject}, context::intrinsics::Intrinsics, error::JsNativeError, - object::JsObject, + object::{JsObject, ObjectData}, string::utf16, symbol::JsSymbol, Context, JsResult, JsValue, @@ -186,14 +186,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); - // 3. Perform ! CreateDataPropertyOrThrow(obj, "value", value). - obj.create_data_property_or_throw(utf16!("value"), value, context) - .expect("this CreateDataPropertyOrThrow call must not fail"); // 4. Perform ! CreateDataPropertyOrThrow(obj, "done", done). - obj.create_data_property_or_throw(utf16!("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/mod.rs b/boa_engine/src/builtins/map/mod.rs index b058f686cbe..18822c27599 100644 --- a/boa_engine/src/builtins/map/mod.rs +++ b/boa_engine/src/builtins/map/mod.rs @@ -40,22 +40,19 @@ impl IntrinsicObject for Map { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); - let get_size = BuiltInBuilder::new(intrinsics) - .callable(Self::get_size) + let get_size = BuiltInBuilder::callable(intrinsics, Self::get_size) .name("get size") .build(); - let entries_function = BuiltInBuilder::new(intrinsics) - .callable(Self::entries) + let entries_function = BuiltInBuilder::callable(intrinsics, Self::entries) .name("entries") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_accessor( JsSymbol::species(), Some(get_species), @@ -135,7 +132,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 0d4191d6edc..cc9b0f1afa3 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -97,6 +97,9 @@ use crate::{ js_string, native_function::{NativeFunction, NativeFunctionPointer}, object::{ + shape::{ + property_table::PropertyTableInner, shared_shape::SharedShape, slot::SlotAttributes, + }, FunctionBinding, JsFunction, JsObject, JsPrototype, ObjectData, CONSTRUCTOR, PROTOTYPE, }, property::{Attribute, PropertyDescriptor, PropertyKey}, @@ -187,8 +190,8 @@ impl Intrinsics { /// Abstract operation [`CreateIntrinsics ( realmRec )`][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-createintrinsics - pub(crate) fn new() -> Self { - let intrinsics = Self::default(); + pub(crate) fn populate(root_shape: &SharedShape) -> Self { + let intrinsics = Self::new(root_shape); BuiltInFunctionObject::init(&intrinsics); BuiltInObjectObject::init(&intrinsics); @@ -520,54 +523,350 @@ struct BuiltInBuilder<'ctx, Kind> { } impl<'ctx> BuiltInBuilder<'ctx, OrdinaryObject> { - fn new(intrinsics: &'ctx Intrinsics) -> BuiltInBuilder<'ctx, OrdinaryObject> { + fn with_intrinsic( + intrinsics: &'ctx Intrinsics, + ) -> BuiltInBuilder<'ctx, OrdinaryObject> { BuiltInBuilder { intrinsics, - object: JsObject::with_null_proto(), + object: I::get(intrinsics), kind: OrdinaryObject, prototype: intrinsics.constructors().object().prototype(), } } +} - fn with_intrinsic( +struct BuiltInConstructorWithPrototype<'ctx> { + intrinsics: &'ctx Intrinsics, + 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, +} + +#[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 + } + + /// Specify the name of the constructor function. + /// + /// Default is `""` + fn name>(mut self, name: N) -> Self { + self.name = name.into(); + self + } + + /// 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.intrinsics, function) + .name(binding.name) + .length(length) + .build(); + + debug_assert!(self + .object_property_table + .map + .get(&binding.binding) + .is_none()); + self.object_property_table.insert( + binding.binding, + SlotAttributes::WRITABLE | SlotAttributes::CONFIGURABLE, + ); + self.object_storage.push(function.into()); + 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 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 + } + + /// Adds a new static accessor property to the builtin object. + fn static_accessor( + mut self, + key: K, + get: Option, + set: Option, + attribute: Attribute, + ) -> Self + where + K: Into, + { + 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 + } + + /// 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.__proto__ = Some(prototype); + self + } + + /// Adds a new method to the constructor's prototype. + fn method(mut self, function: NativeFunctionPointer, binding: B, length: usize) -> Self + where + B: Into, + { + let binding = binding.into(); + let function = BuiltInBuilder::callable(self.intrinsics, function) + .name(binding.name) + .length(length) + .build(); + + debug_assert!(self + .prototype_property_table + .map + .get(&binding.binding) + .is_none()); + self.prototype_property_table.insert( + binding.binding, + SlotAttributes::WRITABLE | SlotAttributes::CONFIGURABLE, + ); + self.prototype_storage.push(function.into()); + self + } + + /// Adds a new data property to the constructor's prototype. + fn property(mut self, key: K, value: V, attribute: Attribute) -> Self + where + K: Into, + V: Into, + { + 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( + mut self, + key: K, + get: Option, + set: Option, + attribute: Attribute, + ) -> Self + where + K: Into, + { + 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 + } + + /// Specifies the parent prototype which objects created by this constructor inherit from. + /// + /// Default is `Object.prototype`. + #[allow(clippy::missing_const_for_fn)] + fn inherits(mut self, prototype: JsPrototype) -> Self { + self.inherits = prototype; + self + } + + /// Specifies the property attributes of the prototype's "constructor" property. + const fn constructor_attributes(mut self, attributes: Attribute) -> Self { + self.attributes = attributes; + self + } + + fn build(mut self) { + let function = function::Function::Native { + function: NativeFunction::from_fn_ptr(self.function), + constructor: (true).then_some(function::ConstructorKind::Base), + }; + + 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.data = ObjectData::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> { + intrinsics: &'ctx Intrinsics, + 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::Function::Native { + function: NativeFunction::from_fn_ptr(self.function), + constructor: None, + }; + + let object = self.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( intrinsics: &'ctx Intrinsics, - ) -> BuiltInBuilder<'ctx, OrdinaryObject> { - BuiltInBuilder { + function: NativeFunctionPointer, + ) -> BuiltInCallable<'ctx> { + BuiltInCallable { intrinsics, - object: I::get(intrinsics), - kind: OrdinaryObject, - prototype: intrinsics.constructors().object().prototype(), + function, + length: 0, + name: js_string!(""), } } - fn with_object( + fn callable_with_intrinsic( intrinsics: &'ctx Intrinsics, - object: JsObject, - ) -> BuiltInBuilder<'ctx, OrdinaryObject> { + function: NativeFunctionPointer, + ) -> BuiltInBuilder<'ctx, Callable> { BuiltInBuilder { intrinsics, - object, - kind: OrdinaryObject, - prototype: intrinsics.constructors().object().prototype(), + object: I::get(intrinsics), + kind: Callable { + function, + name: js_string!(""), + length: 0, + kind: OrdinaryFunction, + }, + prototype: intrinsics.constructors().function().prototype(), } } -} -impl<'ctx> BuiltInBuilder<'ctx, OrdinaryObject> { - fn callable( - self, + fn callable_with_object( + intrinsics: &'ctx Intrinsics, + object: JsObject, function: NativeFunctionPointer, ) -> BuiltInBuilder<'ctx, Callable> { BuiltInBuilder { - intrinsics: self.intrinsics, - object: self.object, + intrinsics, + object, kind: Callable { function, name: js_string!(""), length: 0, kind: OrdinaryFunction, }, - prototype: self.intrinsics.constructors().function().prototype(), + prototype: intrinsics.constructors().function().prototype(), } } } @@ -594,6 +893,27 @@ impl<'ctx> BuiltInBuilder<'ctx, Callable> { } } + fn from_standard_constructor_with_prototype( + intrinsics: &'ctx Intrinsics, + ) -> BuiltInConstructorWithPrototype<'ctx> { + let constructor = SC::STANDARD_CONSTRUCTOR(intrinsics.constructors()); + BuiltInConstructorWithPrototype { + intrinsics, + 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(intrinsics.constructors().function().prototype()), + inherits: Some(intrinsics.constructors().object().prototype()), + attributes: Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + } + } + fn no_proto(self) -> BuiltInBuilder<'ctx, Callable> { BuiltInBuilder { intrinsics: self.intrinsics, @@ -616,8 +936,7 @@ impl BuiltInBuilder<'_, T> { B: Into, { let binding = binding.into(); - let function = BuiltInBuilder::new(self.intrinsics) - .callable(function) + let function = BuiltInBuilder::callable(self.intrinsics, function) .name(binding.name) .length(length) .build(); @@ -648,26 +967,6 @@ impl BuiltInBuilder<'_, T> { self } - /// Adds a new static accessor property to the builtin object. - fn static_accessor( - self, - key: K, - get: Option, - set: Option, - attribute: Attribute, - ) -> Self - where - K: Into, - { - let property = PropertyDescriptor::builder() - .maybe_get(get) - .maybe_set(set) - .enumerable(attribute.enumerable()) - .configurable(attribute.configurable()); - self.object.borrow_mut().insert(key, property); - self - } - /// Specify the `[[Prototype]]` internal field of the builtin object. /// /// Default is `Function.prototype` for constructors and `Object.prototype` for statics. @@ -684,8 +983,7 @@ impl BuiltInBuilder<'_, Callable> { B: Into, { let binding = binding.into(); - let function = BuiltInBuilder::new(self.intrinsics) - .callable(function) + let function = BuiltInBuilder::callable(self.intrinsics, function) .name(binding.name) .length(length) .build(); @@ -715,41 +1013,6 @@ impl BuiltInBuilder<'_, Callable> { self.kind.kind.prototype.borrow_mut().insert(key, property); self } - - /// Adds new accessor property to the constructor's prototype. - fn accessor( - self, - key: K, - get: Option, - set: Option, - attribute: Attribute, - ) -> Self - 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); - self - } - - /// Specifies the parent prototype which objects created by this constructor inherit from. - /// - /// Default is `Object.prototype`. - #[allow(clippy::missing_const_for_fn)] - fn inherits(mut self, prototype: JsPrototype) -> Self { - self.kind.kind.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 - } } impl BuiltInBuilder<'_, Callable> { diff --git a/boa_engine/src/builtins/number/globals.rs b/boa_engine/src/builtins/number/globals.rs index 6ef92ba1a51..49d1383c47a 100644 --- a/boa_engine/src/builtins/number/globals.rs +++ b/boa_engine/src/builtins/number/globals.rs @@ -37,8 +37,7 @@ pub(crate) struct IsFinite; impl IntrinsicObject for IsFinite { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(is_finite) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, is_finite) .name(Self::NAME) .length(1) .build(); @@ -84,8 +83,7 @@ pub(crate) struct IsNaN; impl IntrinsicObject for IsNaN { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(is_nan) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, is_nan) .name(Self::NAME) .length(1) .build(); @@ -226,8 +224,7 @@ pub(crate) struct ParseInt; impl IntrinsicObject for ParseInt { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(parse_int) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, parse_int) .name(Self::NAME) .length(2) .build(); @@ -300,8 +297,7 @@ pub(crate) struct ParseFloat; impl IntrinsicObject for ParseFloat { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(parse_float) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, 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 bb6c8c489cc..fd609c4de8e 100644 --- a/boa_engine/src/builtins/number/mod.rs +++ b/boa_engine/src/builtins/number/mod.rs @@ -50,7 +50,7 @@ impl IntrinsicObject for Number { let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_property(utf16!("EPSILON"), f64::EPSILON, attribute) .static_property( utf16!("MAX_SAFE_INTEGER"), @@ -120,7 +120,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/mod.rs b/boa_engine/src/builtins/object/mod.rs index daa35e76961..3e406546f6c 100644 --- a/boa_engine/src/builtins/object/mod.rs +++ b/boa_engine/src/builtins/object/mod.rs @@ -47,17 +47,15 @@ impl IntrinsicObject for Object { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let legacy_proto_getter = BuiltInBuilder::new(intrinsics) - .callable(Self::legacy_proto_getter) + let legacy_proto_getter = BuiltInBuilder::callable(intrinsics, Self::legacy_proto_getter) .name("get __proto__") .build(); - let legacy_setter_proto = BuiltInBuilder::new(intrinsics) - .callable(Self::legacy_proto_setter) + let legacy_setter_proto = BuiltInBuilder::callable(intrinsics, Self::legacy_proto_setter) .name("set __proto__") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .inherits(None) .accessor( utf16!("__proto__"), @@ -137,7 +135,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()); } diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 6a1a25cde7e..517e18f1fd9 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -311,12 +311,11 @@ impl IntrinsicObject for Promise { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_method(Self::all, "all", 1) .static_method(Self::all_settled, "allSettled", 1) .static_method(Self::any, "any", 1) @@ -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 197dbb3a3f4..f9eda8c930d 100644 --- a/boa_engine/src/builtins/proxy/mod.rs +++ b/boa_engine/src/builtins/proxy/mod.rs @@ -127,7 +127,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 8f70a93fcf6..4baf7d431a7 100644 --- a/boa_engine/src/builtins/regexp/mod.rs +++ b/boa_engine/src/builtins/regexp/mod.rs @@ -47,50 +47,40 @@ impl IntrinsicObject for RegExp { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; - let get_has_indices = BuiltInBuilder::new(intrinsics) - .callable(Self::get_has_indices) + let get_has_indices = BuiltInBuilder::callable(intrinsics, Self::get_has_indices) .name("get hasIndices") .build(); - let get_global = BuiltInBuilder::new(intrinsics) - .callable(Self::get_global) + let get_global = BuiltInBuilder::callable(intrinsics, Self::get_global) .name("get global") .build(); - let get_ignore_case = BuiltInBuilder::new(intrinsics) - .callable(Self::get_ignore_case) + let get_ignore_case = BuiltInBuilder::callable(intrinsics, Self::get_ignore_case) .name("get ignoreCase") .build(); - let get_multiline = BuiltInBuilder::new(intrinsics) - .callable(Self::get_multiline) + let get_multiline = BuiltInBuilder::callable(intrinsics, Self::get_multiline) .name("get multiline") .build(); - let get_dot_all = BuiltInBuilder::new(intrinsics) - .callable(Self::get_dot_all) + let get_dot_all = BuiltInBuilder::callable(intrinsics, Self::get_dot_all) .name("get dotAll") .build(); - let get_unicode = BuiltInBuilder::new(intrinsics) - .callable(Self::get_unicode) + let get_unicode = BuiltInBuilder::callable(intrinsics, Self::get_unicode) .name("get unicode") .build(); - let get_sticky = BuiltInBuilder::new(intrinsics) - .callable(Self::get_sticky) + let get_sticky = BuiltInBuilder::callable(intrinsics, Self::get_sticky) .name("get sticky") .build(); - let get_flags = BuiltInBuilder::new(intrinsics) - .callable(Self::get_flags) + let get_flags = BuiltInBuilder::callable(intrinsics, Self::get_flags) .name("get flags") .build(); - let get_source = BuiltInBuilder::new(intrinsics) - .callable(Self::get_source) + let get_source = BuiltInBuilder::callable(intrinsics, Self::get_source) .name("get source") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_accessor( JsSymbol::species(), Some(get_species), @@ -230,7 +220,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/set/mod.rs b/boa_engine/src/builtins/set/mod.rs index 19418ebe73b..f0d174dc13d 100644 --- a/boa_engine/src/builtins/set/mod.rs +++ b/boa_engine/src/builtins/set/mod.rs @@ -43,22 +43,19 @@ impl IntrinsicObject for Set { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); - let size_getter = BuiltInBuilder::new(intrinsics) - .callable(Self::size_getter) + let size_getter = BuiltInBuilder::callable(intrinsics, Self::size_getter) .name("get size") .build(); - let values_function = BuiltInBuilder::new(intrinsics) - .callable(Self::values) + let values_function = BuiltInBuilder::callable(intrinsics, Self::values) .name("values") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_accessor( JsSymbol::species(), Some(get_species), @@ -126,7 +123,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); diff --git a/boa_engine/src/builtins/set/set_iterator.rs b/boa_engine/src/builtins/set/set_iterator.rs index 4fcf1812fa3..68fe17c045a 100644 --- a/boa_engine/src/builtins/set/set_iterator.rs +++ b/boa_engine/src/builtins/set/set_iterator.rs @@ -80,7 +80,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 5fa46a6f2d7..c75430643ab 100644 --- a/boa_engine/src/builtins/string/mod.rs +++ b/boa_engine/src/builtins/string/mod.rs @@ -70,7 +70,7 @@ impl IntrinsicObject for String { let symbol_iterator = JsSymbol::iterator(); let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .property(utf16!("length"), 0, attribute) .static_method(Self::raw, "raw", 1) .static_method(Self::from_char_code, "fromCharCode", 1) @@ -185,7 +185,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/symbol/mod.rs b/boa_engine/src/builtins/symbol/mod.rs index b1a0bc9b868..2ba837761e8 100644 --- a/boa_engine/src/builtins/symbol/mod.rs +++ b/boa_engine/src/builtins/symbol/mod.rs @@ -110,18 +110,16 @@ impl IntrinsicObject for Symbol { let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - let to_primitive = BuiltInBuilder::new(intrinsics) - .callable(Self::to_primitive) + let to_primitive = BuiltInBuilder::callable(intrinsics, Self::to_primitive) .name("[Symbol.toPrimitive]") .length(1) .build(); - let get_description = BuiltInBuilder::new(intrinsics) - .callable(Self::get_description) + let get_description = BuiltInBuilder::callable(intrinsics, Self::get_description) .name("get description") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_method(Self::for_, "for", 1) .static_method(Self::key_for, "keyFor", 1) .static_property(utf16!("asyncIterator"), symbol_async_iterator, attribute) diff --git a/boa_engine/src/builtins/typed_array/mod.rs b/boa_engine/src/builtins/typed_array/mod.rs index 32f4ba5e273..14ef860b4bc 100644 --- a/boa_engine/src/builtins/typed_array/mod.rs +++ b/boa_engine/src/builtins/typed_array/mod.rs @@ -50,12 +50,11 @@ macro_rules! typed_array { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let get_species = BuiltInBuilder::new(intrinsics) - .callable(TypedArray::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, TypedArray::get_species) .name("get [Symbol.species]") .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .prototype(intrinsics.constructors().typed_array().constructor()) .inherits(Some(intrinsics.constructors().typed_array().prototype())) .static_accessor( @@ -232,54 +231,42 @@ pub(crate) struct TypedArray; impl IntrinsicObject for TypedArray { fn init(intrinsics: &Intrinsics) { - let get_species = BuiltInBuilder::new(intrinsics) - .callable(Self::get_species) + let get_species = BuiltInBuilder::callable(intrinsics, Self::get_species) .name("get [Symbol.species]") .build(); - let get_buffer = BuiltInBuilder::new(intrinsics) - .callable(Self::buffer) + let get_buffer = BuiltInBuilder::callable(intrinsics, Self::buffer) .name("get buffer") .build(); - let get_byte_length = BuiltInBuilder::new(intrinsics) - .callable(Self::byte_length) + let get_byte_length = BuiltInBuilder::callable(intrinsics, Self::byte_length) .name("get byteLength") .build(); - let get_byte_offset = BuiltInBuilder::new(intrinsics) - .callable(Self::byte_offset) + let get_byte_offset = BuiltInBuilder::callable(intrinsics, Self::byte_offset) .name("get byteOffset") .build(); - let get_length = BuiltInBuilder::new(intrinsics) - .callable(Self::length) + let get_length = BuiltInBuilder::callable(intrinsics, Self::length) .name("get length") .build(); - let get_to_string_tag = BuiltInBuilder::new(intrinsics) - .callable(Self::to_string_tag) + let get_to_string_tag = BuiltInBuilder::callable(intrinsics, Self::to_string_tag) .name("get [Symbol.toStringTag]") .build(); - let values_function = BuiltInBuilder::new(intrinsics) - .callable(Self::values) + let values_function = BuiltInBuilder::callable(intrinsics, Self::values) .name("values") .length(0) .build(); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .static_accessor( JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, ) - .property( - utf16!("length"), - 0, - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, - ) .property( JsSymbol::iterator(), values_function, @@ -3196,7 +3183,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 e8350e044bb..0c25223f247 100644 --- a/boa_engine/src/builtins/uri/mod.rs +++ b/boa_engine/src/builtins/uri/mod.rs @@ -79,8 +79,7 @@ pub(crate) struct DecodeUri; impl IntrinsicObject for DecodeUri { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(decode_uri) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, decode_uri) .name(Self::NAME) .length(1) .build(); @@ -98,8 +97,7 @@ pub(crate) struct DecodeUriComponent; impl IntrinsicObject for DecodeUriComponent { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(decode_uri_component) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, decode_uri_component) .name(Self::NAME) .length(1) .build(); @@ -121,8 +119,7 @@ pub(crate) struct EncodeUri; impl IntrinsicObject for EncodeUri { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(encode_uri) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, encode_uri) .name(Self::NAME) .length(1) .build(); @@ -139,8 +136,7 @@ pub(crate) struct EncodeUriComponent; impl IntrinsicObject for EncodeUriComponent { fn init(intrinsics: &Intrinsics) { - BuiltInBuilder::with_intrinsic::(intrinsics) - .callable(encode_uri_component) + BuiltInBuilder::callable_with_intrinsic::(intrinsics, 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 ee4399c2508..80067294875 100644 --- a/boa_engine/src/builtins/weak/weak_ref.rs +++ b/boa_engine/src/builtins/weak/weak_ref.rs @@ -159,7 +159,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 acb4a4ea6bb..4c98200f52d 100644 --- a/boa_engine/src/builtins/weak_map/mod.rs +++ b/boa_engine/src/builtins/weak_map/mod.rs @@ -32,7 +32,7 @@ impl IntrinsicObject for WeakMap { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .property( JsSymbol::to_string_tag(), Self::NAME, @@ -81,7 +81,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 1c5d0873137..a8effa448b1 100644 --- a/boa_engine/src/builtins/weak_set/mod.rs +++ b/boa_engine/src/builtins/weak_set/mod.rs @@ -29,7 +29,7 @@ impl IntrinsicObject for WeakSet { fn init(intrinsics: &Intrinsics) { let _timer = Profiler::global().start_event(Self::NAME, "init"); - BuiltInBuilder::from_standard_constructor::(intrinsics) + BuiltInBuilder::from_standard_constructor_with_prototype::(intrinsics) .property( JsSymbol::to_string_tag(), Self::NAME, @@ -77,7 +77,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/bytecompiler/declaration/declaration_pattern.rs b/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs index ea655f87095..7a75a7e9441 100644 --- a/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs +++ b/boa_engine/src/bytecompiler/declaration/declaration_pattern.rs @@ -38,8 +38,9 @@ impl ByteCompiler<'_, '_> { self.emit_opcode(Opcode::Dup); match name { PropertyName::Literal(name) => { - let index = self.get_or_insert_name((*name).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*name).into()); + + self.emit_get_property_by_name(literal_index); } PropertyName::Computed(node) => { self.compile_expr(node, true); @@ -109,8 +110,9 @@ impl ByteCompiler<'_, '_> { self.emit_opcode(Opcode::Dup); match name { PropertyName::Literal(name) => { - let index = self.get_or_insert_name((*name).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*name).into()); + + self.emit_get_property_by_name(literal_index); } PropertyName::Computed(node) => { self.compile_expr(node, true); @@ -148,8 +150,9 @@ impl ByteCompiler<'_, '_> { self.emit_opcode(Opcode::Dup); match name { PropertyName::Literal(name) => { - let index = self.get_or_insert_name((*name).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*name).into()); + + self.emit_get_property_by_name(literal_index); } PropertyName::Computed(node) => { self.compile_expr(node, true); diff --git a/boa_engine/src/bytecompiler/expression/assign.rs b/boa_engine/src/bytecompiler/expression/assign.rs index c046d670815..13d2c2a3a6d 100644 --- a/boa_engine/src/bytecompiler/expression/assign.rs +++ b/boa_engine/src/bytecompiler/expression/assign.rs @@ -76,12 +76,12 @@ impl ByteCompiler<'_, '_> { Access::Property { access } => match access { PropertyAccess::Simple(access) => match access.field() { PropertyAccessField::Const(name) => { - let index = self.get_or_insert_name((*name).into()); + let literal_index = self.get_or_insert_name((*name).into()); self.compile_expr(access.target(), true); self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::Dup); - self.emit(Opcode::GetPropertyByName, &[index]); + self.emit_get_property_by_name(literal_index); if short_circuit { pop_count = 2; early_exit = Some(self.emit_opcode_with_operand(opcode)); @@ -91,7 +91,7 @@ impl ByteCompiler<'_, '_> { self.emit_opcode(opcode); } - self.emit(Opcode::SetPropertyByName, &[index]); + self.emit(Opcode::SetPropertyByName, &[literal_index]); if !use_expr { self.emit_opcode(Opcode::Pop); } @@ -139,13 +139,13 @@ impl ByteCompiler<'_, '_> { } PropertyAccess::Super(access) => match access.field() { PropertyAccessField::Const(name) => { - let index = self.get_or_insert_name((*name).into()); + let literal_index = self.get_or_insert_name((*name).into()); self.emit_opcode(Opcode::Super); self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::This); self.emit_opcode(Opcode::Swap); - self.emit(Opcode::GetPropertyByName, &[index]); + self.emit_get_property_by_name(literal_index); if short_circuit { pop_count = 2; early_exit = Some(self.emit_opcode_with_operand(opcode)); @@ -155,7 +155,7 @@ impl ByteCompiler<'_, '_> { self.emit_opcode(opcode); } - self.emit(Opcode::SetPropertyByName, &[index]); + self.emit(Opcode::SetPropertyByName, &[literal_index]); if !use_expr { self.emit_opcode(Opcode::Pop); } diff --git a/boa_engine/src/bytecompiler/expression/mod.rs b/boa_engine/src/bytecompiler/expression/mod.rs index 4c285e71878..16d21167825 100644 --- a/boa_engine/src/bytecompiler/expression/mod.rs +++ b/boa_engine/src/bytecompiler/expression/mod.rs @@ -200,8 +200,8 @@ impl ByteCompiler<'_, '_> { self.emit(Opcode::Dup, &[]); match access.field() { PropertyAccessField::Const(field) => { - let index = self.get_or_insert_name((*field).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*field).into()); + self.emit_get_property_by_name(literal_index); } PropertyAccessField::Expr(field) => { self.compile_expr(field, true); diff --git a/boa_engine/src/bytecompiler/expression/update.rs b/boa_engine/src/bytecompiler/expression/update.rs index 1249c6a372c..e387f84dd72 100644 --- a/boa_engine/src/bytecompiler/expression/update.rs +++ b/boa_engine/src/bytecompiler/expression/update.rs @@ -40,19 +40,19 @@ impl ByteCompiler<'_, '_> { Access::Property { access } => match access { PropertyAccess::Simple(access) => match access.field() { PropertyAccessField::Const(name) => { - let index = self.get_or_insert_name((*name).into()); + let literal_index = self.get_or_insert_name((*name).into()); self.compile_expr(access.target(), true); self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::Dup); - self.emit(Opcode::GetPropertyByName, &[index]); + self.emit_get_property_by_name(literal_index); self.emit_opcode(opcode); if post { self.emit_opcode(Opcode::RotateRight); self.emit_u8(4); } - self.emit(Opcode::SetPropertyByName, &[index]); + self.emit(Opcode::SetPropertyByName, &[literal_index]); if post { self.emit_opcode(Opcode::Pop); } @@ -94,20 +94,20 @@ impl ByteCompiler<'_, '_> { } PropertyAccess::Super(access) => match access.field() { PropertyAccessField::Const(name) => { - let index = self.get_or_insert_name((*name).into()); + let literal_index = self.get_or_insert_name((*name).into()); self.emit_opcode(Opcode::Super); self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::This); self.emit_opcode(Opcode::Swap); - self.emit(Opcode::GetPropertyByName, &[index]); + self.emit_get_property_by_name(literal_index); self.emit_opcode(opcode); if post { self.emit_opcode(Opcode::RotateRight); self.emit_u8(3); } - self.emit(Opcode::SetPropertyByName, &[index]); + self.emit(Opcode::SetPropertyByName, &[literal_index]); if post { self.emit_opcode(Opcode::Pop); } diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 84868ca95fc..907c314a9d9 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -9,10 +9,12 @@ mod module; mod statement; mod utils; +use std::cell::Cell; + use crate::{ builtins::function::ThisMode, environments::{BindingLocator, CompileTimeEnvironment}, - vm::{BindingOpcode, CodeBlock, Opcode}, + vm::{BindingOpcode, CodeBlock, InlineCache, Opcode}, Context, JsBigInt, JsString, JsValue, }; use boa_ast::{ @@ -234,7 +236,7 @@ pub struct ByteCompiler<'b, 'host> { pub(crate) params: FormalParameterList, /// Bytecode - pub(crate) bytecode: Vec, + pub(crate) bytecode: Vec>, /// Literals pub(crate) literals: Vec, @@ -272,6 +274,8 @@ pub struct ByteCompiler<'b, 'host> { /// When the execution of the parameter expressions throws an error, we do not need to pop the function environment. pub(crate) function_environment_push_location: u32, + pub(crate) ic: Vec, + literals_map: FxHashMap, names_map: FxHashMap, private_names_map: FxHashMap, @@ -313,6 +317,7 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { is_class_constructor: false, class_field_initializer_name: None, function_environment_push_location: 0, + ic: Vec::default(), literals_map: FxHashMap::default(), names_map: FxHashMap::default(), @@ -446,16 +451,32 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { } } + fn emit_get_property_by_name(&mut self, literal_index: u32) { + let ic_index = self.ic.len() as u32; + self.ic.push(InlineCache::new(literal_index)); + + self.emit_opcode(Opcode::GetPropertyByName); + + self.emit_u32(ic_index); + self.emit_usize(0); + self.emit_u32(0); + self.emit_u8(0); + } + + fn emit_usize(&mut self, value: usize) { + self.bytecode.extend(value.to_ne_bytes().map(Cell::new)); + } + fn emit_u64(&mut self, value: u64) { - self.bytecode.extend(value.to_ne_bytes()); + self.bytecode.extend(value.to_ne_bytes().map(Cell::new)); } fn emit_u32(&mut self, value: u32) { - self.bytecode.extend(value.to_ne_bytes()); + self.bytecode.extend(value.to_ne_bytes().map(Cell::new)); } fn emit_u16(&mut self, value: u16) { - self.bytecode.extend(value.to_ne_bytes()); + self.bytecode.extend(value.to_ne_bytes().map(Cell::new)); } fn emit_opcode(&mut self, opcode: Opcode) { @@ -463,7 +484,7 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { } fn emit_u8(&mut self, value: u8) { - self.bytecode.push(value); + self.bytecode.push(Cell::new(value)); } fn emit_push_integer(&mut self, value: i32) { @@ -550,11 +571,11 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { let Label { index } = label; let index = index as usize; - let bytes = target.to_ne_bytes(); + let bytes = target.to_ne_bytes().map(Cell::new); // This is done to avoid unneeded bounds checks. assert!(self.bytecode.len() > index + U32_SIZE && usize::MAX - U32_SIZE >= index); - self.bytecode[index + 1..=index + U32_SIZE].copy_from_slice(bytes.as_slice()); + self.bytecode[index + 1..=index + U32_SIZE].clone_from_slice(bytes.as_slice()); } fn patch_jump(&mut self, label: Label) { @@ -572,9 +593,9 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { Access::Property { access } => match access { PropertyAccess::Simple(access) => match access.field() { PropertyAccessField::Const(name) => { - let index = self.get_or_insert_name((*name).into()); + let literal_index = self.get_or_insert_name((*name).into()); self.compile_expr(access.target(), true); - self.emit(Opcode::GetPropertyByName, &[index]); + self.emit_get_property_by_name(literal_index); } PropertyAccessField::Expr(expr) => { self.compile_expr(access.target(), true); @@ -589,9 +610,10 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { } PropertyAccess::Super(access) => match access.field() { PropertyAccessField::Const(field) => { - let index = self.get_or_insert_name((*field).into()); + let literal_index = self.get_or_insert_name((*field).into()); self.emit_opcode(Opcode::Super); - self.emit(Opcode::GetPropertyByName, &[index]); + + self.emit_get_property_by_name(literal_index); } PropertyAccessField::Expr(expr) => { self.emit_opcode(Opcode::Super); @@ -823,8 +845,9 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { self.emit_opcode(Opcode::Dup); match access.field() { PropertyAccessField::Const(field) => { - let index = self.get_or_insert_name((*field).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*field).into()); + + self.emit_get_property_by_name(literal_index); } PropertyAccessField::Expr(field) => { self.compile_expr(field, true); @@ -843,8 +866,9 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { self.emit_opcode(Opcode::Super); match access.field() { PropertyAccessField::Const(field) => { - let index = self.get_or_insert_name((*field).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*field).into()); + + self.emit_get_property_by_name(literal_index); } PropertyAccessField::Expr(expr) => { self.compile_expr(expr, true); @@ -928,8 +952,9 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { self.emit_opcode(Opcode::Dup); match field { PropertyAccessField::Const(name) => { - let index = self.get_or_insert_name((*name).into()); - self.emit(Opcode::GetPropertyByName, &[index]); + let literal_index = self.get_or_insert_name((*name).into()); + + self.emit_get_property_by_name(literal_index); } PropertyAccessField::Expr(expr) => { self.compile_expr(expr, true); @@ -1327,6 +1352,7 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { is_class_constructor: self.is_class_constructor, class_field_initializer_name: self.class_field_initializer_name, function_environment_push_location: self.function_environment_push_location, + ic: self.ic.into_boxed_slice(), #[cfg(feature = "trace")] trace: std::cell::Cell::new(false), } diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index c5a00d53fba..2d1a3640315 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -2,19 +2,37 @@ 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. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Intrinsics { /// Cached standard constructors pub(super) constructors: StandardConstructors, /// Cached intrinsic objects pub(super) objects: IntrinsicObjects, + + 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 { @@ -26,6 +44,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. @@ -928,3 +950,200 @@ impl IntrinsicObjects { self.intl.clone() } } + +#[derive(Debug)] +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, + } + } + + pub(crate) const fn iterator_result(&self) -> &ObjectTemplate { + &self.iterator_result + } + pub(crate) const fn ordinary_object(&self) -> &ObjectTemplate { + &self.ordinary_object + } + pub(crate) const fn array(&self) -> &ObjectTemplate { + &self.array + } + pub(crate) const fn number(&self) -> &ObjectTemplate { + &self.number + } + pub(crate) const fn string(&self) -> &ObjectTemplate { + &self.string + } + pub(crate) const fn symbol(&self) -> &ObjectTemplate { + &self.symbol + } + pub(crate) const fn bigint(&self) -> &ObjectTemplate { + &self.bigint + } + pub(crate) const fn boolean(&self) -> &ObjectTemplate { + &self.boolean + } + pub(crate) const fn unmapped_arguments(&self) -> &ObjectTemplate { + &self.unmapped_arguments + } + pub(crate) const fn mapped_arguments(&self) -> &ObjectTemplate { + &self.mapped_arguments + } + pub(crate) const fn function_with_prototype(&self) -> &ObjectTemplate { + &self.function_with_prototype + } + pub(crate) const fn function_prototype(&self) -> &ObjectTemplate { + &self.function_prototype + } + pub(crate) const fn function(&self) -> &ObjectTemplate { + &self.function + } + pub(crate) const fn async_function(&self) -> &ObjectTemplate { + &self.async_function + } + pub(crate) const fn function_without_proto(&self) -> &ObjectTemplate { + &self.function_without_proto + } + 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 2e92181dfc5..6e46a24f84e 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -22,7 +22,8 @@ use crate::{ class::{Class, ClassBuilder}, job::{IdleJobQueue, JobQueue, NativeJob}, native_function::NativeFunction, - object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject}, + object::PropertyMap, + object::{shape::shared_shape::SharedShape, FunctionObjectBuilder, JsObject}, optimizer::{Optimizer, OptimizerOptions, OptimizerStatistics}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, @@ -104,6 +105,7 @@ pub struct Context<'host> { job_queue: &'host dyn JobQueue, optimizer_options: OptimizerOptions, + root_shape: SharedShape, } impl std::fmt::Debug for Context<'_> { @@ -340,7 +342,7 @@ impl Context<'_> { .build(); self.global_bindings_mut().insert( - name.into(), + &PropertyKey::String(name.into()), PropertyDescriptor::builder() .value(function) .writable(true) @@ -372,7 +374,7 @@ impl Context<'_> { .build(); self.global_bindings_mut().insert( - name.into(), + &PropertyKey::String(name.into()), PropertyDescriptor::builder() .value(function) .writable(true) @@ -410,7 +412,8 @@ impl Context<'_> { .configurable(T::ATTRIBUTES.configurable()) .build(); - self.global_bindings_mut().insert(T::NAME.into(), property); + self.global_bindings_mut() + .insert(&PropertyKey::String(T::NAME.into()), property); Ok(()) } @@ -480,13 +483,17 @@ impl Context<'_> { pub fn clear_kept_objects(&mut self) { self.kept_alive.clear(); } + + pub(crate) fn root_shape(&self) -> SharedShape { + self.root_shape.clone() + } } // ==== Private API ==== impl Context<'_> { /// Return a mutable reference to the global object string bindings. - pub(crate) fn global_bindings_mut(&mut self) -> &mut GlobalPropertyMap { + pub(crate) fn global_bindings_mut(&mut self) -> &mut PropertyMap { self.realm.global_bindings_mut() } @@ -654,8 +661,10 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { { let host_hooks = self.host_hooks.unwrap_or(&DefaultHooks); + let root_shape = SharedShape::root(); + let mut context = Context { - realm: Realm::create(host_hooks), + realm: Realm::create(host_hooks, &root_shape), interner: self.interner.unwrap_or_default(), vm: Vm::default(), strict: false, @@ -670,6 +679,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { host_hooks, job_queue: self.job_queue.unwrap_or(&IdleJobQueue), optimizer_options: OptimizerOptions::OPTIMIZE_ALL, + root_shape, }; builtins::set_default_global_bindings(&mut context)?; diff --git a/boa_engine/src/environments/compile.rs b/boa_engine/src/environments/compile.rs index 5b7725295b8..cdb81d00162 100644 --- a/boa_engine/src/environments/compile.rs +++ b/boa_engine/src/environments/compile.rs @@ -1,5 +1,7 @@ use crate::{ - environments::runtime::BindingLocator, property::PropertyDescriptor, Context, JsString, JsValue, + environments::runtime::BindingLocator, + property::{PropertyDescriptor, PropertyKey}, + Context, JsString, JsValue, }; use boa_ast::expression::Identifier; use boa_gc::{Finalize, Gc, GcRefCell, Trace}; @@ -289,14 +291,12 @@ impl Context<'_> { .interner() .resolve_expect(name.sym()) .into_common::(false); - let desc = self - .realm - .global_property_map - .string_property_map() - .get(&name_str); + + let key = PropertyKey::String(name_str); + let desc = self.realm.global_property_map.get(&key); if desc.is_none() { self.global_bindings_mut().insert( - name_str, + &key, PropertyDescriptor::builder() .value(JsValue::Undefined) .writable(true) diff --git a/boa_engine/src/error.rs b/boa_engine/src/error.rs index c5f02ea446a..f754102938f 100644 --- a/boa_engine/src/error.rs +++ b/boa_engine/src/error.rs @@ -669,7 +669,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/jsdataview.rs b/boa_engine/src/object/builtins/jsdataview.rs index 444103a7d0d..14472baf4d7 100644 --- a/boa_engine/src/object/builtins/jsdataview.rs +++ b/boa_engine/src/object/builtins/jsdataview.rs @@ -96,7 +96,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 544a47ff141..77ef87d8b1a 100644 --- a/boa_engine/src/object/builtins/jsdate.rs +++ b/boa_engine/src/object/builtins/jsdate.rs @@ -42,7 +42,11 @@ impl JsDate { #[inline] pub fn new(context: &mut Context<'_>) -> Self { let prototype = context.intrinsics().constructors().date().prototype(); - let inner = JsObject::from_proto_and_data(prototype, ObjectData::date(Date::default())); + let inner = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::date(Date::default()), + ); Self { inner } } @@ -558,7 +562,11 @@ impl JsDate { let date_time = Date::new(Some(date_time.naive_local())); 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/jsgenerator.rs b/boa_engine/src/object/builtins/jsgenerator.rs index 0fccc5d9a4c..a60b329d14b 100644 --- a/boa_engine/src/object/builtins/jsgenerator.rs +++ b/boa_engine/src/object/builtins/jsgenerator.rs @@ -20,7 +20,8 @@ impl JsGenerator { pub fn new(context: &mut Context<'_>) -> Self { let prototype = context.intrinsics().objects().generator(); - let generator = JsObject::from_proto_and_data( + let generator = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), prototype, ObjectData::generator(Generator { state: GeneratorState::Undefined, diff --git a/boa_engine/src/object/builtins/jsmap.rs b/boa_engine/src/object/builtins/jsmap.rs index 5eb184ab970..ee99cbe0067 100644 --- a/boa_engine/src/object/builtins/jsmap.rs +++ b/boa_engine/src/object/builtins/jsmap.rs @@ -184,7 +184,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 8618e5dea68..36cb800e1e5 100644 --- a/boa_engine/src/object/builtins/jspromise.rs +++ b/boa_engine/src/object/builtins/jspromise.rs @@ -204,7 +204,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 ba840f20fd6..295f1e0876f 100644 --- a/boa_engine/src/object/builtins/jsproxy.rs +++ b/boa_engine/src/object/builtins/jsproxy.rs @@ -493,7 +493,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/global.rs b/boa_engine/src/object/internal_methods/global.rs index 41be3c3d052..f9c699fef35 100644 --- a/boa_engine/src/object/internal_methods/global.rs +++ b/boa_engine/src/object/internal_methods/global.rs @@ -254,30 +254,15 @@ pub(crate) fn global_own_property_keys( }; // 2. For each own property key P of O such that P is an array index, in ascending numeric index order, do - // a. Add P as the last element of keys. + // a. Add P as the last element of 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( - context - .realm - .global_property_map - .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( - context - .realm - .global_property_map - .symbol_property_keys() - .cloned() - .map(Into::into), - ); + // a. Add P as the last element of keys. + keys.extend(context.realm.global_property_map.shape.keys()); // 5. Return keys. Ok(keys) diff --git a/boa_engine/src/object/internal_methods/integer_indexed.rs b/boa_engine/src/object/internal_methods/integer_indexed.rs index e5cf99063d5..2e9f5d13117 100644 --- a/boa_engine/src/object/internal_methods/integer_indexed.rs +++ b/boa_engine/src/object/internal_methods/integer_indexed.rs @@ -222,30 +222,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 00279675f66..2bdf3aab716 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -5,7 +5,7 @@ //! //! [spec]: https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots -use super::{JsPrototype, PROTOTYPE}; +use super::{shape::slot::Slot, JsPrototype, PROTOTYPE}; use crate::{ context::intrinsics::{StandardConstructor, StandardConstructors}, object::JsObject, @@ -104,6 +104,28 @@ impl JsObject { func(self, key, context) } + /// Internal method `[[GetOwnProperty]]` + /// + /// Get the specified property of this object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getownproperty-p + pub(crate) fn __get_own_property_slot__( + &self, + slot: Slot, + context: &mut Context<'_>, + ) -> JsResult { + let _timer = Profiler::global().start_event("Object::__get_own_property_slot__", "object"); + let func = self + .borrow() + .data + .internal_methods + .__get_own_property_slot__; + func(self, slot, context) + } + /// Internal method `[[DefineOwnProperty]]` /// /// Define a new property of this object. @@ -160,6 +182,25 @@ impl JsObject { func(self, key, receiver, context) } + /// Internal method `[[Get]]` + /// + /// Get the specified property of this object or its prototype. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver + pub(crate) fn __get_slot__( + &self, + slot: Slot, + receiver: &JsValue, + context: &mut Context<'_>, + ) -> JsResult { + let _timer = Profiler::global().start_event("Object::__get__", "object"); + let func = self.borrow().data.internal_methods.__get_slot__; + func(self, slot, receiver, context) + } + /// Internal method `[[Set]]` /// /// Set the specified property of this object or its prototype to the provided value. @@ -286,6 +327,8 @@ pub(crate) static ORDINARY_INTERNAL_METHODS: InternalObjectMethods = InternalObj __own_property_keys__: ordinary_own_property_keys, __call__: None, __construct__: None, + __get_own_property_slot__: ordinary_get_own_property_slot, + __get_slot__: ordinary_get_slot, }; /// The internal representation of the internal methods of a `JsObject`. @@ -316,6 +359,10 @@ pub(crate) struct InternalObjectMethods { Option) -> JsResult>, pub(crate) __construct__: Option) -> JsResult>, + + pub(crate) __get_own_property_slot__: + fn(&JsObject, Slot, &mut Context<'_>) -> JsResult, + pub(crate) __get_slot__: fn(&JsObject, Slot, &JsValue, &mut Context<'_>) -> JsResult, } /// Abstract operation `OrdinaryGetPrototypeOf`. @@ -348,14 +395,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]]. @@ -384,7 +429,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. @@ -454,6 +499,36 @@ pub(crate) fn ordinary_get_own_property( Ok(obj.borrow().properties.get(key)) } +/// Abstract operation `OrdinaryGetOwnProperty`. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ordinarygetownproperty +#[allow(clippy::unnecessary_wraps)] +pub(crate) fn ordinary_get_own_property_slot( + obj: &JsObject, + slot: Slot, + _context: &mut Context<'_>, +) -> JsResult { + let _timer = Profiler::global().start_event("Object::ordinary_get_own_property", "object"); + // 1. Assert: IsPropertyKey(P) is true. + // 2. If O does not have an own property with key P, return undefined. + // 3. Let D be a newly created Property Descriptor with no fields. + // 4. Let X be O's own property whose key is P. + // 5. If X is a data property, then + // a. Set D.[[Value]] to the value of X's [[Value]] attribute. + // b. Set D.[[Writable]] to the value of X's [[Writable]] attribute. + // 6. Else, + // a. Assert: X is an accessor property. + // b. Set D.[[Get]] to the value of X's [[Get]] attribute. + // c. Set D.[[Set]] to the value of X's [[Set]] attribute. + // 7. Set D.[[Enumerable]] to the value of X's [[Enumerable]] attribute. + // 8. Set D.[[Configurable]] to the value of X's [[Configurable]] attribute. + // 9. Return D. + Ok(obj.borrow().properties.get_storage(slot)) +} + /// Abstract operation `OrdinaryDefineOwnProperty`. /// /// More information: @@ -556,6 +631,38 @@ pub(crate) fn ordinary_get( } } +/// Abstract operation `OrdinaryGet`. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ordinaryget +pub(crate) fn ordinary_get_slot( + obj: &JsObject, + slot: Slot, + receiver: &JsValue, + context: &mut Context<'_>, +) -> JsResult { + let _timer = Profiler::global().start_event("Object::ordinary_get", "object"); + + let desc = obj.__get_own_property_slot__(slot, context)?; + + match desc.kind() { + // 4. If IsDataDescriptor(desc) is true, return desc.[[Value]]. + DescriptorKind::Data { + value: Some(value), .. + } => Ok(value.clone()), + // 5. Assert: IsAccessorDescriptor(desc) is true. + // 6. Let getter be desc.[[Get]]. + DescriptorKind::Accessor { get: Some(get), .. } if !get.is_undefined() => { + // 8. Return ? Call(getter, Receiver). + get.call(receiver, &[], context) + } + // 7. If getter is undefined, return undefined. + _ => Ok(JsValue::undefined()), + } +} + /// Abstract operation `OrdinarySet`. /// /// More information: @@ -714,24 +821,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/proxy.rs b/boa_engine/src/object/internal_methods/proxy.rs index 6874d8d5d9d..453bfa94b62 100644 --- a/boa_engine/src/object/internal_methods/proxy.rs +++ b/boa_engine/src/object/internal_methods/proxy.rs @@ -30,6 +30,10 @@ pub(crate) static PROXY_EXOTIC_INTERNAL_METHODS_BASIC: InternalObjectMethods = __own_property_keys__: proxy_exotic_own_property_keys, __call__: None, __construct__: None, + __get_own_property_slot__: |_, _, _| { + todo!("Not implemented __get_own_property_slot__ not implemented") + }, + __get_slot__: |_, _, _, _| todo!("Not implemented proxy __get_slot__"), }; pub(crate) static PROXY_EXOTIC_INTERNAL_METHODS_WITH_CALL: InternalObjectMethods = diff --git a/boa_engine/src/object/internal_methods/string.rs b/boa_engine/src/object/internal_methods/string.rs index c4151741694..0c919b5fab5 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 ca4f497a2fc..38926336b36 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -2,7 +2,10 @@ //! //! The `JsObject` is a garbage collected Object. -use super::{JsPrototype, NativeObject, Object, PropertyMap}; +use super::{ + shape::{shared_shape::SharedShape, Shape}, + JsPrototype, NativeObject, Object, PropertyMap, +}; use crate::{ error::JsNativeError, object::{ObjectData, ObjectKind}, @@ -17,6 +20,7 @@ use std::{ collections::HashMap, error::Error, fmt::{self, Debug, Display}, + hash::Hash, result::Result as StdResult, }; use thin_vec::ThinVec; @@ -72,9 +76,33 @@ impl JsObject { Self { inner: Gc::new(GcRefCell::new(Object { data, - prototype: prototype.into(), extensible: true, - properties: PropertyMap::default(), + properties: PropertyMap::from_prototype_unique_shape(prototype.into()), + private_elements: ThinVec::new(), + })), + } + } + + /// 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(GcRefCell::new(Object { + data, + extensible: true, + properties: PropertyMap::from_prototype_with_shared_shape( + Shape::shared(root_shape), + prototype.into(), + ), private_elements: ThinVec::new(), })), } @@ -273,8 +301,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. @@ -701,7 +729,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, @@ -711,9 +739,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, @@ -781,6 +809,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 dd862edbdf4..992b0f96a3d 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -8,21 +8,24 @@ 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}, + global::GLOBAL_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}, - global::GLOBAL_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::{ @@ -72,6 +75,7 @@ pub mod builtins; mod jsobject; mod operations; mod property_map; +pub mod shape; pub(crate) use builtins::*; @@ -95,6 +99,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`. @@ -123,8 +132,6 @@ pub struct Object { pub data: ObjectData, /// The collection of properties contained in the object properties: PropertyMap, - /// Instance prototype `__proto__`. - prototype: JsPrototype, /// Whether it can have new properties added to it. extensible: bool, /// The `[[PrivateElements]]` internal slot. @@ -135,7 +142,6 @@ unsafe impl Trace for Object { boa_gc::custom_trace!(this, { mark(&this.data); mark(&this.properties); - mark(&this.prototype); for (_, element) in &this.private_elements { mark(element); } @@ -780,7 +786,6 @@ impl Default for Object { Self { data: ObjectData::ordinary(), properties: PropertyMap::default(), - prototype: None, extensible: true, private_elements: ThinVec::default(), } @@ -788,6 +793,11 @@ impl Default for Object { } impl Object { + /// 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 { @@ -1620,8 +1630,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. @@ -1633,12 +1643,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 } } @@ -1836,9 +1846,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, @@ -1848,7 +1858,7 @@ impl Object { /// Helper function for property removal. #[inline] - pub(crate) fn remove(&mut self, key: &PropertyKey) -> Option { + pub(crate) fn remove(&mut self, key: &PropertyKey) -> bool { self.properties.remove(key) } @@ -2016,22 +2026,12 @@ 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(), + let object = self.context.intrinsics().templates().function().create( ObjectData::function(self.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) } } @@ -2079,7 +2079,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), ); diff --git a/boa_engine/src/object/property_map.rs b/boa_engine/src/object/property_map.rs index d9923f8ef19..a7fd066d66e 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; @@ -6,10 +14,6 @@ use rustc_hash::{FxHashMap, FxHasher}; use std::{collections::hash_map, hash::BuildHasherDefault, iter::FusedIterator}; use thin_vec::ThinVec; -/// Type alias to make it easier to work with the string properties on the global object. -pub(crate) type GlobalPropertyMap = - IndexMap>; - /// Wrapper around `indexmap::IndexMap` for usage in `PropertyMap`. #[derive(Debug, Finalize)] struct OrderedHashMap(IndexMap>); @@ -63,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 { @@ -99,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 @@ -122,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 @@ -143,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 @@ -182,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. @@ -226,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(), + } + } + + /// TOOD: doc + #[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(), + } + } + + /// TOOD: doc + #[must_use] + #[inline] + pub fn from_prototype_with_shared_shape(mut shape: Shape, prototype: JsPrototype) -> Self { + shape = shape.change_prototype_transition(prototype); + Self { + indexed_properties: IndexedProperties::default(), + 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. @@ -291,65 +417,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. @@ -377,50 +444,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; } - } - - pub(crate) const fn string_property_map(&self) -> &GlobalPropertyMap { - &self.string_properties.0 - } - pub(crate) fn string_property_map_mut(&mut self) -> &mut GlobalPropertyMap { - &mut self.string_properties.0 + false } } diff --git a/boa_engine/src/object/shape/mod.rs b/boa_engine/src/object/shape/mod.rs new file mode 100644 index 00000000000..c82d5a9c00a --- /dev/null +++ b/boa_engine/src/object/shape/mod.rs @@ -0,0 +1,220 @@ +//! 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, +} + +/// The default [`Shape`] type is [`UniqueShape`]. +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`]. + /// + /// NOTE: This assumes that there already is a property with the given key! + 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 00000000000..cb2d7513042 --- /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 00000000000..ba9c9488c83 --- /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 00000000000..0908bc1235f --- /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 00000000000..76ce369de92 --- /dev/null +++ b/boa_engine/src/object/shape/shared_shape/template.rs @@ -0,0 +1,150 @@ +use boa_gc::{Gc, GcRefCell}; +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)] +pub(crate) struct ObjectTemplate { + shape: SharedShape, + storage_len: usize, +} + +impl ObjectTemplate { + /// Create a new [`ObjectTemplate`] + pub(crate) fn new(root_shape: &SharedShape) -> Self { + Self { + shape: root_shape.clone(), + storage_len: 0, + } + } + + /// 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, + storage_len: 0, + } + } + + /// 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.storage_len += 1; + 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.storage_len += 2; + 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 { + data, + extensible: true, + properties: PropertyMap::new(Shape::shared(self.shape.clone()), ThinVec::default()), + private_elements: ThinVec::new(), + }; + + debug_assert_eq!(self.storage_len, storage.len()); + + object.properties.storage = storage; + + Gc::new(GcRefCell::new(object)).into() + } + + /// 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 { + data, + extensible: true, + properties: PropertyMap::new(Shape::shared(self.shape.clone()), elements), + private_elements: ThinVec::new(), + }; + + debug_assert_eq!(self.storage_len, storage.len()); + + object.properties.storage = storage; + + Gc::new(GcRefCell::new(object)).into() + } +} diff --git a/boa_engine/src/object/shape/slot.rs b/boa_engine/src/object/shape/slot.rs new file mode 100644 index 00000000000..0ac65f3de34 --- /dev/null +++ b/boa_engine/src/object/shape/slot.rs @@ -0,0 +1,80 @@ +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; + + const DIRECT = 0b0010_0000; + const NOT_CACHABLE = 0b0100_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 a [`SlotAttribute`] field of an element in a dense 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 00000000000..5e052fb02c7 --- /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 ac816d172a6..fe67effba1f 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 fd639b6857b..f0f7bf26afe 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; @@ -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 e76c323cbf9..dbffa97ff24 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -9,7 +9,7 @@ use crate::{ context::{intrinsics::Intrinsics, HostHooks}, environments::{CompileTimeEnvironment, DeclarativeEnvironmentStack}, - object::{GlobalPropertyMap, JsObject, PropertyMap}, + object::{shape::shared_shape::SharedShape, JsObject, PropertyMap}, }; use boa_gc::{Gc, GcRefCell}; use boa_profiler::Profiler; @@ -30,10 +30,10 @@ pub struct Realm { 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::new(); + let intrinsics = Intrinsics::populate(root_shape); let global_object = hooks.create_global_object(&intrinsics); let global_this = hooks @@ -62,8 +62,8 @@ impl Realm { &self.global_this } - pub(crate) fn global_bindings_mut(&mut self) -> &mut GlobalPropertyMap { - self.global_property_map.string_property_map_mut() + pub(crate) fn global_bindings_mut(&mut self) -> &mut PropertyMap { + &mut self.global_property_map } /// Set the number of bindings on the global environment. diff --git a/boa_engine/src/value/conversions/serde_json.rs b/boa_engine/src/value/conversions/serde_json.rs index 153c05c2680..7f2187a55b1 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 ea54b0ffc1c..313f31b2b4f 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 6536b1a77ae..c657037b1cd 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 c27dedd30f8..f302f466b4a 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 6e404e04d01..0e69999bff1 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 b99aebacb65..27ca6fe156e 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -13,7 +13,7 @@ use crate::{ environments::{BindingLocator, CompileTimeEnvironment}, error::JsNativeError, object::{ - internal_methods::get_prototype_from_constructor, JsObject, ObjectData, CONSTRUCTOR, + internal_methods::get_prototype_from_constructor, shape::Shape, JsObject, ObjectData, PROTOTYPE, }, property::PropertyDescriptor, @@ -28,7 +28,7 @@ use boa_ast::{ use boa_gc::{Finalize, Gc, GcRefCell, Trace}; use boa_interner::Sym; use boa_profiler::Profiler; -use std::{collections::VecDeque, mem::size_of}; +use std::{cell::Cell, collections::VecDeque, mem::size_of}; use thin_vec::ThinVec; #[cfg(any(feature = "trace", feature = "flowgraph"))] @@ -58,6 +58,25 @@ unsafe impl Readable for i64 {} unsafe impl Readable for f32 {} unsafe impl Readable for f64 {} +unsafe impl Readable for usize {} + +/// TODO: doc +#[derive(Clone, Debug, Trace, Finalize)] +#[allow(dead_code)] +pub struct InlineCache { + pub(crate) literal_index: u32, + pub(crate) shape: GcRefCell>, +} + +impl InlineCache { + pub(crate) const fn new(literal_index: u32) -> Self { + Self { + shape: GcRefCell::new(None), + literal_index, + } + } +} + /// The internal representation of a JavaScript function. /// /// A `CodeBlock` is generated for each function compiled by the @@ -86,7 +105,8 @@ pub struct CodeBlock { pub(crate) params: FormalParameterList, /// Bytecode - pub(crate) bytecode: Box<[u8]>, + #[unsafe_ignore_trace] + pub(crate) bytecode: Box<[Cell]>, /// Literals pub(crate) literals: Box<[JsValue]>, @@ -129,10 +149,13 @@ pub struct CodeBlock { /// When the execution of the parameter expressions throws an error, we do not need to pop the function environment. pub(crate) function_environment_push_location: u32, + /// inline caching + pub(crate) ic: Box<[InlineCache]>, + #[cfg(feature = "trace")] /// Trace instruction execution to `stdout`. #[unsafe_ignore_trace] - pub(crate) trace: std::cell::Cell, + pub(crate) trace: Cell, } impl CodeBlock { @@ -158,6 +181,7 @@ impl CodeBlock { is_class_constructor: false, class_field_initializer_name: None, function_environment_push_location: 0, + ic: Box::default(), #[cfg(feature = "trace")] trace: std::cell::Cell::new(false), } @@ -206,13 +230,37 @@ impl CodeBlock { unsafe { self.read_unchecked(offset) } } + /// TODO: doc + pub(crate) fn write_usize(&self, offset: usize, value: usize) { + let bytes = value.to_ne_bytes(); + for (bytecode, byte) in self.bytecode[offset..offset + size_of::()] + .iter() + .zip(bytes.into_iter()) + { + bytecode.set(byte); + } + } + + /// TODO: doc + pub(crate) fn write_u32(&self, offset: usize, value: u32) { + let bytes = value.to_ne_bytes(); + for (bytecode, byte) in self.bytecode[offset..offset + size_of::()] + .iter() + .zip(bytes.into_iter()) + { + bytecode.set(byte); + } + } + /// Get the operands after the `Opcode` pointed to by `pc` as a `String`. /// Modifies the `pc` to point to the next instruction. /// /// Returns an empty `String` if no operands are present. #[cfg(any(feature = "trace", feature = "flowgraph"))] pub(crate) fn instruction_operands(&self, pc: &mut usize, interner: &Interner) -> String { - let opcode: Opcode = self.bytecode[*pc].try_into().expect("invalid opcode"); + use crate::object::shape::slot::SlotAttributes; + + let opcode: Opcode = self.bytecode[*pc].get().try_into().expect("invalid opcode"); *pc += size_of::(); match opcode { Opcode::SetFunctionName => { @@ -325,8 +373,26 @@ impl CodeBlock { interner.resolve_expect(self.bindings[operand as usize].name().sym()), ) } - Opcode::GetPropertyByName - | Opcode::GetMethod + Opcode::GetPropertyByName => { + let operand = self.read::(*pc); + *pc += size_of::(); + let shape_ptr = self.read::(*pc); + *pc += size_of::(); + let slot_index = self.read::(*pc); + *pc += size_of::(); + let slot_attributes = self.read::(*pc); + *pc += size_of::(); + + let slot_attributes = + SlotAttributes::from_bits(slot_attributes).expect("should not fail!"); + + let ic = &self.ic[operand as usize]; + format!( + "{operand:04}: '{}', Shape: {shape_ptr}, Slot: index: {slot_index}, attributes {slot_attributes:?}", + interner.resolve_expect(self.names[ic.literal_index as usize].sym()), + ) + } + Opcode::GetMethod | Opcode::SetPropertyByName | Opcode::DefineOwnPropertyByName | Opcode::DefineClassStaticMethodByName @@ -493,7 +559,7 @@ impl ToInternedString for CodeBlock { let mut pc = 0; let mut count = 0; while pc < self.bytecode.len() { - let opcode: Opcode = self.bytecode[pc].try_into().expect("invalid opcode"); + let opcode: Opcode = self.bytecode[pc].get().try_into().expect("invalid opcode"); let opcode = opcode.as_str(); let previous_pc = pc; let operands = self.instruction_operands(&mut pc, interner); @@ -555,38 +621,97 @@ 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() + let Some(prototype) = prototype else { + // fast path + return create_function_object_fast(code, r#async, arrow, method, context); + }; + + let name: JsValue = context + .interner() + .resolve_expect(code.name) + .into_common::(false) + .into(); + + let length: JsValue = code.length.into(); + + let function = if r#async { + let promise_capability = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("cannot fail per spec"); + + Function::Async { + code, + environments: context.realm.environments.clone(), + home_object: None, + promise_capability, + class_object: None, + } } else { - context.intrinsics().constructors().function().prototype() + Function::Ordinary { + code, + environments: context.realm.environments.clone(), + constructor_kind: ConstructorKind::Base, + home_object: None, + fields: ThinVec::new(), + private_methods: ThinVec::new(), + class_object: None, + } }; - let name_property = PropertyDescriptor::builder() - .value( - context - .interner() - .resolve_expect(code.name) - .into_common::(false), + let data = ObjectData::function(function); + + let templates = context.intrinsics().templates(); + + let (mut template, storage, constructor_prototype) = if r#async || arrow || method { + ( + templates.function_without_proto().clone(), + vec![length, name], + None, ) - .writable(false) - .enumerable(false) - .configurable(true) - .build(); + } else { + let constructor_prototype = templates + .function_prototype() + .create(ObjectData::ordinary(), vec![JsValue::undefined()]); - let length_property = PropertyDescriptor::builder() - .value(code.length) - .writable(false) - .enumerable(false) - .configurable(true) - .build(); + 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 +} + +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 { let promise_capability = PromiseCapability::new( @@ -614,41 +739,37 @@ 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(); + 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 - .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"); - - if !r#async && !arrow && !method { - let prototype = JsObject::with_object_proto(context); - 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"); } - - constructor } /// Creates a new generator function object. diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs index bbeb1038a08..deb5eab32a7 100644 --- a/boa_engine/src/vm/flowgraph/mod.rs +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -32,7 +32,7 @@ impl CodeBlock { let mut pc = 0; while pc < self.bytecode.len() { - let opcode: Opcode = self.bytecode[pc].try_into().expect("invalid opcode"); + let opcode: Opcode = self.bytecode[pc].get().try_into().expect("invalid opcode"); let opcode_str = opcode.as_str(); let previous_pc = pc; @@ -380,8 +380,26 @@ impl CodeBlock { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } - Opcode::GetPropertyByName - | Opcode::GetMethod + Opcode::GetPropertyByName => { + // TODO: Implement better trace format + let operand = self.read::(pc); + pc += size_of::(); + let _shape_ptr = self.read::(pc); + pc += size_of::(); + let _slot_index = self.read::(pc); + pc += size_of::(); + let _slot_attributes = self.read::(pc); + pc += size_of::(); + + let ic = &self.ic[operand as usize]; + let label = format!( + "{opcode_str} '{}'", + interner.resolve_expect(self.names[ic.literal_index as usize].sym()), + ); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::GetMethod | Opcode::SetPropertyByName | Opcode::DefineOwnPropertyByName | Opcode::DefineClassStaticMethodByName diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 2884f8e002a..c18552dd862 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -27,7 +27,11 @@ mod opcode; #[cfg(feature = "flowgraph")] pub mod flowgraph; -pub use {call_frame::CallFrame, code_block::CodeBlock, opcode::Opcode}; +pub use { + call_frame::CallFrame, + code_block::{CodeBlock, InlineCache}, + opcode::Opcode, +}; pub(crate) use { call_frame::GeneratorResumeKind, @@ -88,6 +92,26 @@ impl Vm { value } + #[track_caller] + pub(crate) fn read_and_get_location(&mut self) -> (T, usize) { + let location = self.frame().pc; + let value = self.frame().code_block.read::(location); + self.frame_mut().pc += size_of::(); + (value, location) + } + + pub(crate) fn write_usize(&self, location: usize, value: usize) { + self.frame().code_block.write_usize(location, value); + } + + pub(crate) fn write_u32(&self, location: usize, value: u32) { + self.frame().code_block.write_u32(location, value); + } + + pub(crate) fn write_u8(&self, location: usize, value: u8) { + self.frame().code_block.bytecode[location].set(value); + } + /// Retrieves the VM frame /// /// # Panics @@ -127,6 +151,7 @@ impl Context<'_> { let opcode: Opcode = { let _timer = Profiler::global().start_event("Opcode retrieval", "vm"); let opcode = self.vm.frame().code_block.bytecode[self.vm.frame().pc] + .get() .try_into() .expect("could not convert code at PC to opcode"); self.vm.frame_mut().pc += 1; diff --git a/boa_engine/src/vm/opcode/define/mod.rs b/boa_engine/src/vm/opcode/define/mod.rs index 30adcab0665..dc6561df4ab 100644 --- a/boa_engine/src/vm/opcode/define/mod.rs +++ b/boa_engine/src/vm/opcode/define/mod.rs @@ -1,5 +1,5 @@ use crate::{ - property::PropertyDescriptor, + property::{PropertyDescriptor, PropertyKey}, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsString, JsValue, }; @@ -30,14 +30,19 @@ impl Operation for DefVar { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - context.global_bindings_mut().entry(key).or_insert( - PropertyDescriptor::builder() - .value(JsValue::Undefined) - .writable(true) - .enumerable(true) - .configurable(true) - .build(), - ); + + let key = PropertyKey::String(key); + if context.global_bindings_mut().get(&key).is_none() { + context.global_bindings_mut().insert( + &key, + PropertyDescriptor::builder() + .value(JsValue::Undefined) + .writable(true) + .enumerable(true) + .configurable(true) + .build(), + ); + }; } else { context.realm.environments.put_value_if_uninitialized( binding_locator.environment_index(), diff --git a/boa_engine/src/vm/opcode/get/function.rs b/boa_engine/src/vm/opcode/get/function.rs index 83cc8b92a1d..dd32a0d04e3 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/get/name.rs b/boa_engine/src/vm/opcode/get/name.rs index 7a128716bb2..6e5c078da7b 100644 --- a/boa_engine/src/vm/opcode/get/name.rs +++ b/boa_engine/src/vm/opcode/get/name.rs @@ -1,6 +1,6 @@ use crate::{ error::JsNativeError, - property::DescriptorKind, + property::{DescriptorKind, PropertyKey}, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsString, JsValue, }; @@ -29,7 +29,10 @@ impl Operation for GetName { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - match context.global_bindings_mut().get(&key) { + match context + .global_bindings_mut() + .get(&PropertyKey::String(key.clone())) + { Some(desc) => match desc.kind() { DescriptorKind::Data { value: Some(value), .. @@ -98,7 +101,7 @@ impl Operation for GetNameOrUndefined { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - match context.global_bindings_mut().get(&key) { + match context.global_bindings_mut().get(&PropertyKey::String(key)) { Some(desc) => match desc.kind() { DescriptorKind::Data { value: Some(value), .. diff --git a/boa_engine/src/vm/opcode/get/property.rs b/boa_engine/src/vm/opcode/get/property.rs index b5de8900fbd..69f71a156ec 100644 --- a/boa_engine/src/vm/opcode/get/property.rs +++ b/boa_engine/src/vm/opcode/get/property.rs @@ -1,5 +1,6 @@ use crate::{ js_string, + object::shape::slot::{Slot, SlotAttributes}, property::PropertyKey, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsValue, @@ -17,7 +18,10 @@ impl Operation for GetPropertyByName { const INSTRUCTION: &'static str = "INST - GetPropertyByName"; fn execute(context: &mut Context<'_>) -> JsResult { - let index = context.vm.read::(); + let ic_index = context.vm.read::(); + let (shape_ptr, shape_ptr_location) = context.vm.read_and_get_location::(); + let (slot_index, slot_index_location) = context.vm.read_and_get_location::(); + let (slot_attributes, slot_attributes_locations) = context.vm.read_and_get_location::(); let value = context.vm.pop(); let object = if let Some(object) = value.as_object() { @@ -26,10 +30,71 @@ impl Operation for GetPropertyByName { value.to_object(context)? }; - let name = context.vm.frame().code_block.names[index as usize]; + if slot_attributes & SlotAttributes::NOT_CACHABLE.bits() != 0 { + // TODO: passible optimization, inline sym into bytecode, since it fits in the shape pointer. + let ic = &context.vm.frame().code_block.ic[ic_index as usize]; + let name = context.vm.frame().code_block.names[ic.literal_index as usize]; + let key: PropertyKey = context.interner().resolve_expect(name.sym()).utf16().into(); + let result = object.__get__(&key, value, context)?; + context.vm.push(result); + return Ok(CompletionType::Normal); + } + + // Use inline cache. + if shape_ptr == object.borrow().properties().shape.to_addr_usize() { + let slot = Slot { + index: slot_index, + attributes: SlotAttributes::from_bits_retain(slot_attributes), + }; + let mut result = if slot.attributes.contains(SlotAttributes::DIRECT) { + object.borrow().properties().storage[slot.index as usize].clone() + } else { + let prototype = object + .borrow() + .properties() + .shape + .prototype() + .expect("prototype should have value"); + let prototype = prototype.borrow(); + prototype.properties().storage[slot.index as usize].clone() + }; + if slot.attributes.has_get() { + result = result.call(&value, &[], context)?; + } + context.vm.push(result); + return Ok(CompletionType::Normal); + } + + let ic = &context.vm.frame().code_block.ic[ic_index as usize]; + let name = context.vm.frame().code_block.names[ic.literal_index as usize]; let key: PropertyKey = context.interner().resolve_expect(name.sym()).utf16().into(); - let result = object.__get__(&key, value, context)?; + // Check if it can be cached. + let shape = object.borrow().properties().shape.clone(); + let slot = if let Some(mut slot) = shape.lookup(&key) { + slot.attributes |= SlotAttributes::DIRECT; + slot + } else if let Some(mut slot) = shape.prototype().and_then(|prototype| prototype.borrow().properties().shape.lookup(&key)) { + slot.attributes.set(SlotAttributes::DIRECT, false); + slot + } else { + let result = object.__get__(&key, value, context)?; + context.vm.push(result); + return Ok(CompletionType::Normal); + }; + + // Write inline to bytecode inline. + context + .vm + .write_usize(shape_ptr_location, shape.to_addr_usize()); + context.vm.write_u32(slot_index_location, slot.index); + context + .vm + .write_u8(slot_attributes_locations, slot.attributes.bits()); + + *ic.shape.borrow_mut() = Some(shape); + + let result = object.__get__(&key, value, context)?; context.vm.push(result); Ok(CompletionType::Normal) } diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index f1cdded9f5e..70bc14724ae 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -712,7 +712,7 @@ generate_impl! { /// /// Like `object.name` /// - /// Operands: name_index: `u32` + /// Operands: inline_cache_index: `u32`, shape_ptr: usize, slot_index: u32, slot_attributes: u8 /// /// Stack: object **=>** value GetPropertyByName, diff --git a/boa_engine/src/vm/opcode/push/array.rs b/boa_engine/src/vm/opcode/push/array.rs index 18e5fa3e937..db5b40334b1 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 9ec4b2bda03..46089038eda 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); + 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 c8a4753404e..a88fbcd663b 100644 --- a/boa_engine/src/vm/opcode/set/class_prototype.rs +++ b/boa_engine/src/vm/opcode/set/class_prototype.rs @@ -32,7 +32,12 @@ 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/name.rs b/boa_engine/src/vm/opcode/set/name.rs index e3f99f76a29..7ed206e22b7 100644 --- a/boa_engine/src/vm/opcode/set/name.rs +++ b/boa_engine/src/vm/opcode/set/name.rs @@ -1,5 +1,6 @@ use crate::{ error::JsNativeError, + property::PropertyKey, vm::{opcode::Operation, CompletionType}, Context, JsResult, JsString, }; @@ -30,7 +31,10 @@ impl Operation for SetName { .interner() .resolve_expect(binding_locator.name().sym()) .into_common(false); - let exists = context.global_bindings_mut().contains_key(&key); + let exists = context + .global_bindings_mut() + .get(&PropertyKey::String(key.clone())) + .is_some(); if !exists && context.vm.frame().code_block.strict { return Err(JsNativeError::reference() diff --git a/docs/boa_object.md b/docs/boa_object.md index 52c7c843927..aa40f698612 100644 --- a/docs/boa_object.md +++ b/docs/boa_object.md @@ -148,3 +148,42 @@ Optimizer { 2 >> ``` + +## 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 fomrat. + +```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 00000000000..dd9af8db401 --- /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` has been omitted and it's shape. + +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.