diff --git a/Libraries/LibJS/Bytecode/Interpreter.cpp b/Libraries/LibJS/Bytecode/Interpreter.cpp index 9f23bdf3312..92627013ec7 100644 --- a/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -922,27 +923,6 @@ ALWAYS_INLINE Completion throw_null_or_undefined_property_access(VM& vm, Value b return vm.throw_completion(ErrorType::ToObjectNullOrUndefined); } -ALWAYS_INLINE GC::Ptr base_object_for_get_impl(VM& vm, Value base_value) -{ - if (base_value.is_object()) [[likely]] - return base_value.as_object(); - - // OPTIMIZATION: For various primitives we can avoid actually creating a new object for them. - auto& realm = *vm.current_realm(); - if (base_value.is_string()) - return realm.intrinsics().string_prototype(); - if (base_value.is_number()) - return realm.intrinsics().number_prototype(); - if (base_value.is_boolean()) - return realm.intrinsics().boolean_prototype(); - if (base_value.is_bigint()) - return realm.intrinsics().bigint_prototype(); - if (base_value.is_symbol()) - return realm.intrinsics().symbol_prototype(); - - return nullptr; -} - ALWAYS_INLINE ThrowCompletionOr> base_object_for_get(VM& vm, Value base_value, Optional base_identifier, IdentifierTableIndex property_identifier, Executable const& executable) { if (auto base_object = base_object_for_get_impl(vm, base_value)) @@ -961,92 +941,6 @@ ALWAYS_INLINE ThrowCompletionOr> base_object_for_get(VM& vm, Val return throw_null_or_undefined_property_get(vm, base_value, base_identifier, property, executable); } -enum class GetByIdMode { - Normal, - Length, -}; - -template -inline ThrowCompletionOr get_by_id(VM& vm, Optional base_identifier, IdentifierTableIndex property, Value base_value, Value this_value, PropertyLookupCache& cache, Executable const& executable) -{ - if constexpr (mode == GetByIdMode::Length) { - if (base_value.is_string()) { - return Value(base_value.as_string().length_in_utf16_code_units()); - } - } - - auto base_obj = TRY(base_object_for_get(vm, base_value, base_identifier, property, executable)); - - if constexpr (mode == GetByIdMode::Length) { - // OPTIMIZATION: Fast path for the magical "length" property on Array objects. - if (base_obj->has_magical_length_property()) { - return Value { base_obj->indexed_properties().array_like_size() }; - } - } - - auto& shape = base_obj->shape(); - - GC::Ptr prototype_chain_validity; - if (shape.prototype()) - prototype_chain_validity = shape.prototype()->shape().prototype_chain_validity(); - - for (auto& cache_entry : cache.entries) { - if (cache_entry.prototype) { - // OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. - bool can_use_cache = [&]() -> bool { - if (&shape != cache_entry.shape) - return false; - if (!cache_entry.prototype_chain_validity) - return false; - if (!cache_entry.prototype_chain_validity->is_valid()) - return false; - return true; - }(); - if (can_use_cache) { - auto value = cache_entry.prototype->get_direct(cache_entry.property_offset.value()); - if (value.is_accessor()) - return TRY(call(vm, value.as_accessor().getter(), this_value)); - return value; - } - } else if (&shape == cache_entry.shape) { - // OPTIMIZATION: If the shape of the object hasn't changed, we can use the cached property offset. - auto value = base_obj->get_direct(cache_entry.property_offset.value()); - if (value.is_accessor()) - return TRY(call(vm, value.as_accessor().getter(), this_value)); - return value; - } - } - - CacheableGetPropertyMetadata cacheable_metadata; - auto value = TRY(base_obj->internal_get(executable.get_identifier(property), this_value, &cacheable_metadata)); - - // If internal_get() caused object's shape change, we can no longer be sure - // that collected metadata is valid, e.g. if getter in prototype chain added - // property with the same name into the object itself. - if (&shape == &base_obj->shape()) { - auto get_cache_slot = [&] -> PropertyLookupCache::Entry& { - for (size_t i = cache.entries.size() - 1; i >= 1; --i) { - cache.entries[i] = cache.entries[i - 1]; - } - cache.entries[0] = {}; - return cache.entries[0]; - }; - if (cacheable_metadata.type == CacheableGetPropertyMetadata::Type::GetOwnProperty) { - auto& entry = get_cache_slot(); - entry.shape = shape; - entry.property_offset = cacheable_metadata.property_offset.value(); - } else if (cacheable_metadata.type == CacheableGetPropertyMetadata::Type::GetPropertyInPrototypeChain) { - auto& entry = get_cache_slot(); - entry.shape = &base_obj->shape(); - entry.property_offset = cacheable_metadata.property_offset.value(); - entry.prototype = *cacheable_metadata.prototype; - entry.prototype_chain_validity = *prototype_chain_validity; - } - } - - return value; -} - inline ThrowCompletionOr get_by_value(VM& vm, Optional base_identifier, Value base_value, Value property_key_value, Executable const& executable) { // OPTIMIZATION: Fast path for simple Int32 indexes in array-like objects. @@ -2637,7 +2531,7 @@ ThrowCompletionOr GetById::execute_impl(Bytecode::Interpreter& interpreter auto base_value = interpreter.get(base()); auto& cache = interpreter.current_executable().property_lookup_caches[m_cache_index]; - interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), m_base_identifier, m_property, base_value, base_value, cache, interpreter.current_executable()))); + interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), [&] { return interpreter.get_identifier(m_base_identifier); }, [&] { return interpreter.get_identifier(m_property); }, base_value, base_value, cache))); return {}; } @@ -2646,7 +2540,7 @@ ThrowCompletionOr GetByIdWithThis::execute_impl(Bytecode::Interpreter& int auto base_value = interpreter.get(m_base); auto this_value = interpreter.get(m_this_value); auto& cache = interpreter.current_executable().property_lookup_caches[m_cache_index]; - interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), {}, m_property, base_value, this_value, cache, interpreter.current_executable()))); + interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), [] { return Optional {}; }, [&] { return interpreter.get_identifier(m_property); }, base_value, this_value, cache))); return {}; } @@ -2656,7 +2550,7 @@ ThrowCompletionOr GetLength::execute_impl(Bytecode::Interpreter& interpret auto& executable = interpreter.current_executable(); auto& cache = executable.property_lookup_caches[m_cache_index]; - interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), m_base_identifier, *executable.length_identifier, base_value, base_value, cache, executable))); + interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), [&] { return interpreter.get_identifier(m_base_identifier); }, [&] { return executable.get_identifier(*executable.length_identifier); }, base_value, base_value, cache))); return {}; } @@ -2666,7 +2560,7 @@ ThrowCompletionOr GetLengthWithThis::execute_impl(Bytecode::Interpreter& i auto this_value = interpreter.get(m_this_value); auto& executable = interpreter.current_executable(); auto& cache = executable.property_lookup_caches[m_cache_index]; - interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), {}, *executable.length_identifier, base_value, this_value, cache, executable))); + interpreter.set(dst(), TRY(get_by_id(interpreter.vm(), [] { return Optional {}; }, [&] { return executable.get_identifier(*executable.length_identifier); }, base_value, this_value, cache))); return {}; } diff --git a/Libraries/LibJS/Bytecode/PropertyAccess.h b/Libraries/LibJS/Bytecode/PropertyAccess.h new file mode 100644 index 00000000000..a5c9759fe5d --- /dev/null +++ b/Libraries/LibJS/Bytecode/PropertyAccess.h @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021-2025, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Bytecode { + +enum class GetByIdMode { + Normal, + Length, +}; + +ALWAYS_INLINE GC::Ptr base_object_for_get_impl(VM& vm, Value base_value) +{ + if (base_value.is_object()) [[likely]] + return base_value.as_object(); + + // OPTIMIZATION: For various primitives we can avoid actually creating a new object for them. + auto& realm = *vm.current_realm(); + if (base_value.is_string()) + return realm.intrinsics().string_prototype(); + if (base_value.is_number()) + return realm.intrinsics().number_prototype(); + if (base_value.is_boolean()) + return realm.intrinsics().boolean_prototype(); + if (base_value.is_bigint()) + return realm.intrinsics().bigint_prototype(); + if (base_value.is_symbol()) + return realm.intrinsics().symbol_prototype(); + + return nullptr; +} + +template +ALWAYS_INLINE Completion throw_null_or_undefined_property_get(VM& vm, Value base_value, GetBaseIdentifier get_base_identifier, GetPropertyName get_property_name) +{ + VERIFY(base_value.is_nullish()); + + auto base_identifier = get_base_identifier(); + if (base_identifier.has_value()) + return vm.throw_completion(ErrorType::ToObjectNullOrUndefinedWithPropertyAndName, get_property_name(), base_value, base_identifier); + return vm.throw_completion(ErrorType::ToObjectNullOrUndefinedWithProperty, get_property_name(), base_value); +} + +template +ALWAYS_INLINE ThrowCompletionOr> base_object_for_get(VM& vm, Value base_value, GetBaseIdentifier get_base_identifier, GetPropertyName get_property_name) +{ + if (auto base_object = base_object_for_get_impl(vm, base_value)) + return GC::Ref { *base_object }; + + // NOTE: At this point this is guaranteed to throw (null or undefined). + return throw_null_or_undefined_property_get(vm, base_value, get_base_identifier, get_property_name); +} + +template +ALWAYS_INLINE ThrowCompletionOr get_by_id(VM& vm, GetBaseIdentifier get_base_identifier, GetPropertyName get_property_name, Value base_value, Value this_value, PropertyLookupCache& cache) +{ + if constexpr (mode == GetByIdMode::Length) { + if (base_value.is_string()) { + return Value(base_value.as_string().length_in_utf16_code_units()); + } + } + + auto base_obj = TRY(base_object_for_get(vm, base_value, get_base_identifier, get_property_name)); + + if constexpr (mode == GetByIdMode::Length) { + // OPTIMIZATION: Fast path for the magical "length" property on Array objects. + if (base_obj->has_magical_length_property()) { + return Value { base_obj->indexed_properties().array_like_size() }; + } + } + + auto& shape = base_obj->shape(); + + GC::Ptr prototype_chain_validity; + if (shape.prototype()) + prototype_chain_validity = shape.prototype()->shape().prototype_chain_validity(); + + for (auto& cache_entry : cache.entries) { + if (cache_entry.prototype) { + // OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. + bool can_use_cache = [&]() -> bool { + if (&shape != cache_entry.shape) [[unlikely]] + return false; + if (!cache_entry.prototype_chain_validity) [[unlikely]] + return false; + if (!cache_entry.prototype_chain_validity->is_valid()) [[unlikely]] + return false; + return true; + }(); + if (can_use_cache) [[likely]] { + auto value = cache_entry.prototype->get_direct(cache_entry.property_offset.value()); + if (value.is_accessor()) + return TRY(call(vm, value.as_accessor().getter(), this_value)); + return value; + } + } else if (&shape == cache_entry.shape) { + // OPTIMIZATION: If the shape of the object hasn't changed, we can use the cached property offset. + auto value = base_obj->get_direct(cache_entry.property_offset.value()); + if (value.is_accessor()) { + return TRY(call(vm, value.as_accessor().getter(), this_value)); + } + return value; + } + } + + CacheableGetPropertyMetadata cacheable_metadata; + auto value = TRY(base_obj->internal_get(get_property_name(), this_value, &cacheable_metadata)); + + // If internal_get() caused object's shape change, we can no longer be sure + // that collected metadata is valid, e.g. if getter in prototype chain added + // property with the same name into the object itself. + if (&shape == &base_obj->shape()) { + auto get_cache_slot = [&] -> PropertyLookupCache::Entry& { + for (size_t i = cache.entries.size() - 1; i >= 1; --i) { + cache.entries[i] = cache.entries[i - 1]; + } + cache.entries[0] = {}; + return cache.entries[0]; + }; + if (cacheable_metadata.type == CacheableGetPropertyMetadata::Type::GetOwnProperty) { + auto& entry = get_cache_slot(); + entry.shape = shape; + entry.property_offset = cacheable_metadata.property_offset.value(); + } else if (cacheable_metadata.type == CacheableGetPropertyMetadata::Type::GetPropertyInPrototypeChain) { + auto& entry = get_cache_slot(); + entry.shape = &base_obj->shape(); + entry.property_offset = cacheable_metadata.property_offset.value(); + entry.prototype = *cacheable_metadata.prototype; + entry.prototype_chain_validity = *prototype_chain_validity; + } + } + + return value; +} + +} diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 88600522012..4f43028de39 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -327,6 +327,7 @@ class Generator; class Instruction; class Interpreter; class Operand; +struct PropertyLookupCache; class RegexTable; class Register; diff --git a/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Libraries/LibJS/Runtime/AbstractOperations.cpp index 77725517ebc..7bd2c4a14dd 100644 --- a/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -127,7 +127,8 @@ ThrowCompletionOr length_of_array_like(VM& vm, Object const& object) return object.indexed_properties().array_like_size(); // 1. Return โ„(? ToLength(? Get(obj, "length"))). - return TRY(object.get(vm.names.length)).to_length(vm); + static Bytecode::PropertyLookupCache cache; + return TRY(object.get(vm.names.length, cache)).to_length(vm); } // 7.3.20 CreateListFromArrayLike ( obj [ , elementTypes ] ), https://tc39.es/ecma262/#sec-createlistfromarraylike @@ -173,7 +174,8 @@ ThrowCompletionOr> create_list_from_array_like(VM& vm, Val ThrowCompletionOr species_constructor(VM& vm, Object const& object, FunctionObject& default_constructor) { // 1. Let C be ? Get(O, "constructor"). - auto constructor = TRY(object.get(vm.names.constructor)); + static Bytecode::PropertyLookupCache cache; + auto constructor = TRY(object.get(vm.names.constructor, cache)); // 2. If C is undefined, return defaultConstructor. if (constructor.is_undefined()) @@ -184,7 +186,8 @@ ThrowCompletionOr species_constructor(VM& vm, Object const& obj return vm.throw_completion(ErrorType::NotAConstructor, constructor.to_string_without_side_effects()); // 4. Let S be ? Get(C, @@species). - auto species = TRY(constructor.as_object().get(vm.well_known_symbol_species())); + static Bytecode::PropertyLookupCache cache2; + auto species = TRY(constructor.as_object().get(vm.well_known_symbol_species(), cache2)); // 5. If S is either undefined or null, return defaultConstructor. if (species.is_nullish()) @@ -407,7 +410,8 @@ ThrowCompletionOr get_prototype_from_constructor(VM& vm, FunctionObject // 1. Assert: intrinsicDefaultProto is this specification's name of an intrinsic object. The corresponding object must be an intrinsic that is intended to be used as the [[Prototype]] value of an object. // 2. Let proto be ? Get(constructor, "prototype"). - auto prototype = TRY(constructor.get(vm.names.prototype)); + static Bytecode::PropertyLookupCache cache; + auto prototype = TRY(constructor.get(vm.names.prototype, cache)); // 3. If Type(proto) is not Object, then if (!prototype.is_object()) { diff --git a/Libraries/LibJS/Runtime/ArrayPrototype.cpp b/Libraries/LibJS/Runtime/ArrayPrototype.cpp index 02b8cfe573e..3b7cbe7b421 100644 --- a/Libraries/LibJS/Runtime/ArrayPrototype.cpp +++ b/Libraries/LibJS/Runtime/ArrayPrototype.cpp @@ -119,7 +119,8 @@ static ThrowCompletionOr array_species_create(VM& vm, Object& original_ if (!is_array) return TRY(Array::create(realm, length)).ptr(); - auto constructor = TRY(original_array.get(vm.names.constructor)); + static Bytecode::PropertyLookupCache cache; + auto constructor = TRY(original_array.get(vm.names.constructor, cache)); if (constructor.is_constructor()) { auto& constructor_function = constructor.as_function(); auto* this_realm = vm.current_realm(); @@ -131,7 +132,8 @@ static ThrowCompletionOr array_species_create(VM& vm, Object& original_ } if (constructor.is_object()) { - constructor = TRY(constructor.as_object().get(vm.well_known_symbol_species())); + static Bytecode::PropertyLookupCache cache2; + constructor = TRY(constructor.as_object().get(vm.well_known_symbol_species(), cache2)); if (constructor.is_null()) constructor = js_undefined(); } diff --git a/Libraries/LibJS/Runtime/AsyncGenerator.cpp b/Libraries/LibJS/Runtime/AsyncGenerator.cpp index 658cf7c9aba..8a46cd14d8a 100644 --- a/Libraries/LibJS/Runtime/AsyncGenerator.cpp +++ b/Libraries/LibJS/Runtime/AsyncGenerator.cpp @@ -22,7 +22,8 @@ ThrowCompletionOr> AsyncGenerator::create(Realm& realm, { auto& vm = realm.vm(); // This is "g1.prototype" in figure-2 (https://tc39.es/ecma262/img/figure-2.png) - auto generating_function_prototype = TRY(generating_function->get(vm.names.prototype)); + static Bytecode::PropertyLookupCache cache; + auto generating_function_prototype = TRY(generating_function->get(vm.names.prototype, cache)); auto generating_function_prototype_object = TRY(generating_function_prototype.to_object(vm)); auto object = realm.create(realm, generating_function_prototype_object, move(execution_context)); object->m_generating_function = generating_function; diff --git a/Libraries/LibJS/Runtime/GeneratorObject.cpp b/Libraries/LibJS/Runtime/GeneratorObject.cpp index e7749ea5bd7..be78154c221 100644 --- a/Libraries/LibJS/Runtime/GeneratorObject.cpp +++ b/Libraries/LibJS/Runtime/GeneratorObject.cpp @@ -29,7 +29,8 @@ ThrowCompletionOr> GeneratorObject::create(Realm& realm // changed thus we hardcode the prototype. generating_function_prototype = realm.intrinsics().generator_prototype(); } else { - generating_function_prototype = TRY(generating_function->get(vm.names.prototype)); + static Bytecode::PropertyLookupCache cache; + generating_function_prototype = TRY(generating_function->get(vm.names.prototype, cache)); } auto generating_function_prototype_object = TRY(generating_function_prototype.to_object(vm)); auto object = realm.create(realm, generating_function_prototype_object, move(execution_context)); diff --git a/Libraries/LibJS/Runtime/Iterator.cpp b/Libraries/LibJS/Runtime/Iterator.cpp index 4d6af6c0206..13c72b367df 100644 --- a/Libraries/LibJS/Runtime/Iterator.cpp +++ b/Libraries/LibJS/Runtime/Iterator.cpp @@ -39,7 +39,8 @@ Iterator::Iterator(Object& prototype) ThrowCompletionOr> get_iterator_direct(VM& vm, Object& object) { // 1. Let nextMethod be ? Get(obj, "next"). - auto next_method = TRY(object.get(vm.names.next)); + static Bytecode::PropertyLookupCache cache; + auto next_method = TRY(object.get(vm.names.next, cache)); // 2. Let iteratorRecord be Record { [[Iterator]]: obj, [[NextMethod]]: nextMethod, [[Done]]: false }. // 3. Return iteratorRecord. @@ -57,7 +58,8 @@ ThrowCompletionOr> get_iterator_from_method(VM& vm, Valu return vm.throw_completion(ErrorType::NotIterable, object.to_string_without_side_effects()); // 3. Let nextMethod be ? Get(iterator, "next"). - auto next_method = TRY(iterator.get(vm, vm.names.next)); + static Bytecode::PropertyLookupCache cache; + auto next_method = TRY(iterator.get(vm, vm.names.next, cache)); // 4. Let iteratorRecord be the Iterator Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }. auto iterator_record = vm.heap().allocate(iterator.as_object(), next_method, false); @@ -79,7 +81,8 @@ ThrowCompletionOr> get_iterator(VM& vm, Value object, It // b. If method is undefined, then if (!method) { // i. Let syncMethod be ? GetMethod(obj, @@iterator). - auto sync_method = TRY(object.get_method(vm, vm.well_known_symbol_iterator())); + static Bytecode::PropertyLookupCache cache; + auto sync_method = TRY(object.get_method(vm, vm.well_known_symbol_iterator(), cache)); // ii. If syncMethod is undefined, throw a TypeError exception. if (!sync_method) @@ -95,7 +98,8 @@ ThrowCompletionOr> get_iterator(VM& vm, Value object, It // 2. Else, else { // a. Let method be ? GetMethod(obj, @@iterator). - method = TRY(object.get_method(vm, vm.well_known_symbol_iterator())); + static Bytecode::PropertyLookupCache cache; + method = TRY(object.get_method(vm, vm.well_known_symbol_iterator(), cache)); } // 3. If method is undefined, throw a TypeError exception. @@ -124,7 +128,8 @@ ThrowCompletionOr> get_iterator_flattenable(VM& vm, Valu } // 2. Let method be ? GetMethod(obj, %Symbol.iterator%). - auto method = TRY(object.get_method(vm, vm.well_known_symbol_iterator())); + static Bytecode::PropertyLookupCache cache; + auto method = TRY(object.get_method(vm, vm.well_known_symbol_iterator(), cache)); Value iterator; @@ -192,14 +197,16 @@ ThrowCompletionOr> iterator_next(VM& vm, IteratorRecord& iterato ThrowCompletionOr iterator_complete(VM& vm, Object& iterator_result) { // 1. Return ToBoolean(? Get(iterResult, "done")). - return TRY(iterator_result.get(vm.names.done)).to_boolean(); + static Bytecode::PropertyLookupCache cache; + return TRY(iterator_result.get(vm.names.done, cache)).to_boolean(); } // 7.4.8 IteratorValue ( iteratorResult ), https://tc39.es/ecma262/#sec-iteratorvalue ThrowCompletionOr iterator_value(VM& vm, Object& iterator_result) { // 1. Return ? Get(iterResult, "value"). - return TRY(iterator_result.get(vm.names.value)); + static Bytecode::PropertyLookupCache cache; + return TRY(iterator_result.get(vm.names.value, cache)); } // 7.4.9 IteratorStep ( iteratorRecord ), https://tc39.es/ecma262/#sec-iteratorstep @@ -220,7 +227,8 @@ ThrowCompletionOr iterator_step(VM& vm, IteratorRecord& i auto result = TRY(iterator_next(vm, iterator_record)); // 2. Let done be Completion(IteratorComplete(result)). - auto done = result->get(vm.names.done); + static Bytecode::PropertyLookupCache cache; + auto done = result->get(vm.names.done, cache); // 3. If done is a throw completion, then if (done.is_throw_completion()) { @@ -244,7 +252,8 @@ ThrowCompletionOr iterator_step(VM& vm, IteratorRecord& i } // 6. Return result. - return ThrowCompletionOr { IterationResult { done_value, result->get(vm.names.value) } }; + static Bytecode::PropertyLookupCache cache2; + return ThrowCompletionOr { IterationResult { done_value, result->get(vm.names.value, cache2) } }; } // 7.4.10 IteratorStepValue ( iteratorRecord ), https://tc39.es/ecma262/#sec-iteratorstepvalue diff --git a/Libraries/LibJS/Runtime/Object.cpp b/Libraries/LibJS/Runtime/Object.cpp index 95150ddfa05..1b3a44267e3 100644 --- a/Libraries/LibJS/Runtime/Object.cpp +++ b/Libraries/LibJS/Runtime/Object.cpp @@ -125,6 +125,13 @@ ThrowCompletionOr Object::get(PropertyKey const& property_key) const return TRY(internal_get(property_key, this)); } +// 7.3.2 Get ( O, P ), https://tc39.es/ecma262/#sec-get-o-p +ThrowCompletionOr Object::get(PropertyKey const& property_key, Bytecode::PropertyLookupCache& cache) const +{ + // 1. Return ? O.[[Get]](P, O). + return TRY(Value(this).get(vm(), property_key, cache)); +} + // NOTE: 7.3.3 GetV ( V, P ) is implemented as Value::get(). // 7.3.4 Set ( O, P, V, Throw ), https://tc39.es/ecma262/#sec-set-o-p-v-throw @@ -1540,7 +1547,15 @@ ThrowCompletionOr Object::ordinary_to_primitive(Value::PreferredType pref // 3. For each element name of methodNames, do for (auto& method_name : method_names) { // a. Let method be ? Get(O, name). - auto method = TRY(get(method_name)); + Value method; + if (method_name == vm.names.toString) { + static Bytecode::PropertyLookupCache cache; + method = TRY(get(method_name, cache)); + } else { + ASSERT(method_name == vm.names.valueOf); + static Bytecode::PropertyLookupCache cache; + method = TRY(get(method_name, cache)); + } // b. If IsCallable(method) is true, then if (method.is_function()) { diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index 90f4087cb4a..020ba1e15b4 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -121,6 +121,7 @@ public: // 7.3 Operations on Objects, https://tc39.es/ecma262/#sec-operations-on-objects ThrowCompletionOr get(PropertyKey const&) const; + ThrowCompletionOr get(PropertyKey const&, Bytecode::PropertyLookupCache&) const; ThrowCompletionOr set(PropertyKey const&, Value, ShouldThrowExceptions); ThrowCompletionOr create_data_property(PropertyKey const&, Value, Optional* new_property_offset = nullptr); void create_method_property(PropertyKey const&, Value); diff --git a/Libraries/LibJS/Runtime/ObjectPrototype.cpp b/Libraries/LibJS/Runtime/ObjectPrototype.cpp index 469265959c5..0dae946630b 100644 --- a/Libraries/LibJS/Runtime/ObjectPrototype.cpp +++ b/Libraries/LibJS/Runtime/ObjectPrototype.cpp @@ -182,7 +182,8 @@ JS_DEFINE_NATIVE_FUNCTION(ObjectPrototype::to_string) builtin_tag = "Object"sv; // 15. Let tag be ? Get(O, @@toStringTag). - auto to_string_tag = TRY(object->get(vm.well_known_symbol_to_string_tag())); + static Bytecode::PropertyLookupCache cache; + auto to_string_tag = TRY(object->get(vm.well_known_symbol_to_string_tag(), cache)); // Optimization: Instead of creating another PrimitiveString from builtin_tag, we separate tag and to_string_tag and add an additional branch to step 16. StringView tag; diff --git a/Libraries/LibJS/Runtime/RegExpPrototype.cpp b/Libraries/LibJS/Runtime/RegExpPrototype.cpp index 64c1ffd8af9..10d7613ab83 100644 --- a/Libraries/LibJS/Runtime/RegExpPrototype.cpp +++ b/Libraries/LibJS/Runtime/RegExpPrototype.cpp @@ -61,7 +61,8 @@ void RegExpPrototype::initialize(Realm& realm) static ThrowCompletionOr increment_last_index(VM& vm, Object& regexp_object, Utf16View const& string, bool unicode) { // Let thisIndex be โ„(? ToLength(? Get(rx, "lastIndex"))). - auto last_index_value = TRY(regexp_object.get(vm.names.lastIndex)); + static Bytecode::PropertyLookupCache cache; + auto last_index_value = TRY(regexp_object.get(vm.names.lastIndex, cache)); auto last_index = TRY(last_index_value.to_length(vm)); // Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode). @@ -176,7 +177,8 @@ static ThrowCompletionOr regexp_builtin_exec(VM& vm, RegExpObject& regexp // 1. Let length be the length of S. // 2. Let lastIndex be โ„(? ToLength(? Get(R, "lastIndex"))). - auto last_index_value = TRY(regexp_object.get(vm.names.lastIndex)); + static Bytecode::PropertyLookupCache cache; + auto last_index_value = TRY(regexp_object.get(vm.names.lastIndex, cache)); auto last_index = TRY(last_index_value.to_length(vm)); auto const& regex = regexp_object.regex(); @@ -393,7 +395,8 @@ static ThrowCompletionOr regexp_builtin_exec(VM& vm, RegExpObject& regexp ThrowCompletionOr regexp_exec(VM& vm, Object& regexp_object, GC::Ref string) { // 1. Let exec be ? Get(R, "exec"). - auto exec = TRY(regexp_object.get(vm.names.exec)); + static Bytecode::PropertyLookupCache cache; + auto exec = TRY(regexp_object.get(vm.names.exec, cache)); // 2. If IsCallable(exec) is true, then if (exec.is_function()) { @@ -508,10 +511,13 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::flags) // 17. If unicodeSets is true, append the code unit 0x0076 (LATIN SMALL LETTER V) as the last code unit of result. // 18. Let sticky be ToBoolean(? Get(R, "sticky")). // 19. If sticky is true, append the code unit 0x0079 (LATIN SMALL LETTER Y) as the last code unit of result. -#define __JS_ENUMERATE(FlagName, flagName, flag_name, flag_char) \ - auto flag_##flag_name = TRY(regexp_object->get(vm.names.flagName)); \ - if (flag_##flag_name.to_boolean()) \ - builder.append(#flag_char##sv); +#define __JS_ENUMERATE(FlagName, flagName, flag_name, flag_char) \ + { \ + static Bytecode::PropertyLookupCache cache; \ + auto flag_##flag_name = TRY(regexp_object->get(vm.names.flagName, cache)); \ + if (flag_##flag_name.to_boolean()) \ + builder.append(#flag_char##sv); \ + } JS_ENUMERATE_REGEXP_FLAGS #undef __JS_ENUMERATE @@ -532,7 +538,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_match) auto string = TRY(vm.argument(0).to_primitive_string(vm)); // 4. Let flags be ? ToString(? Get(rx, "flags")). - auto flags_value = TRY(regexp_object->get(vm.names.flags)); + static Bytecode::PropertyLookupCache cache; + auto flags_value = TRY(regexp_object->get(vm.names.flags, cache)); auto flags = TRY(flags_value.to_string(vm)); // 5. If flags does not contain "g", then @@ -608,7 +615,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_match_all) auto* constructor = TRY(species_constructor(vm, regexp_object, realm.intrinsics().regexp_constructor())); // 5. Let flags be ? ToString(? Get(R, "flags")). - auto flags_value = TRY(regexp_object->get(vm.names.flags)); + static Bytecode::PropertyLookupCache cache; + auto flags_value = TRY(regexp_object->get(vm.names.flags, cache)); auto flags = TRY(flags_value.to_string(vm)); // Steps 9-12 are performed early so that flags can be moved. @@ -625,7 +633,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_match_all) auto matcher = TRY(construct(vm, *constructor, regexp_object, PrimitiveString::create(vm, move(flags)))); // 7. Let lastIndex be ? ToLength(? Get(R, "lastIndex")). - auto last_index_value = TRY(regexp_object->get(vm.names.lastIndex)); + static Bytecode::PropertyLookupCache cache2; + auto last_index_value = TRY(regexp_object->get(vm.names.lastIndex, cache2)); auto last_index = TRY(last_index_value.to_length(vm)); // 8. Perform ? Set(matcher, "lastIndex", lastIndex, true). @@ -659,7 +668,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace) } // 7. Let flags be ? ToString(? Get(rx, "flags")). - auto flags_value = TRY(regexp_object->get(vm.names.flags)); + static Bytecode::PropertyLookupCache cache; + auto flags_value = TRY(regexp_object->get(vm.names.flags, cache)); auto flags = TRY(flags_value.to_string(vm)); // 8. If flags contains "g", let global be true. Otherwise, let global be false. @@ -731,7 +741,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace) auto matched_length = matched->length_in_utf16_code_units(); // e. Let position be ? ToIntegerOrInfinity(? Get(result, "index")). - auto position_value = TRY(result->get(vm.names.index)); + static Bytecode::PropertyLookupCache cache2; + auto position_value = TRY(result->get(vm.names.index, cache2)); double position = TRY(position_value.to_integer_or_infinity(vm)); // f. Set position to the result of clamping position between 0 and lengthS. @@ -760,7 +771,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace) } // j. Let namedCaptures be ? Get(result, "groups"). - auto named_captures = TRY(result->get(vm.names.groups)); + static Bytecode::PropertyLookupCache cache3; + auto named_captures = TRY(result->get(vm.names.groups, cache3)); String replacement; @@ -833,7 +845,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_search) auto string = TRY(vm.argument(0).to_primitive_string(vm)); // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). - auto previous_last_index = TRY(regexp_object->get(vm.names.lastIndex)); + static Bytecode::PropertyLookupCache cache; + auto previous_last_index = TRY(regexp_object->get(vm.names.lastIndex, cache)); // 5. If SameValue(previousLastIndex, +0๐”ฝ) is false, then if (!same_value(previous_last_index, Value(0))) { @@ -845,7 +858,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_search) auto result = TRY(regexp_exec(vm, regexp_object, string)); // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). - auto current_last_index = TRY(regexp_object->get(vm.names.lastIndex)); + static Bytecode::PropertyLookupCache cache2; + auto current_last_index = TRY(regexp_object->get(vm.names.lastIndex, cache2)); // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then if (!same_value(current_last_index, previous_last_index)) { @@ -858,7 +872,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_search) return Value(-1); // 10. Return ? Get(result, "index"). - return TRY(result.get(vm, vm.names.index)); + static Bytecode::PropertyLookupCache cache3; + return TRY(result.get(vm, vm.names.index, cache3)); } // 22.2.6.13 get RegExp.prototype.source, https://tc39.es/ecma262/#sec-get-regexp.prototype.source @@ -903,7 +918,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_split) auto* constructor = TRY(species_constructor(vm, regexp_object, realm.intrinsics().regexp_constructor())); // 5. Let flags be ? ToString(? Get(rx, "flags")). - auto flags_value = TRY(regexp_object->get(vm.names.flags)); + static Bytecode::PropertyLookupCache cache; + auto flags_value = TRY(regexp_object->get(vm.names.flags, cache)); auto flags = TRY(flags_value.to_string(vm)); // 6. If flags contains "u" or flags contains "v", let unicodeMatching be true. @@ -973,7 +989,8 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_split) // d. Else, // i. Let e be โ„(? ToLength(? Get(splitter, "lastIndex"))). - auto last_index_value = TRY(splitter->get(vm.names.lastIndex)); + static Bytecode::PropertyLookupCache cache2; + auto last_index_value = TRY(splitter->get(vm.names.lastIndex, cache2)); auto last_index = TRY(last_index_value.to_length(vm)); // ii. Set e to min(e, size). @@ -1068,11 +1085,13 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::to_string) auto regexp_object = TRY(this_object(vm)); // 3. Let pattern be ? ToString(? Get(R, "source")). - auto source_attr = TRY(regexp_object->get(vm.names.source)); + static Bytecode::PropertyLookupCache cache; + auto source_attr = TRY(regexp_object->get(vm.names.source, cache)); auto pattern = TRY(source_attr.to_string(vm)); // 4. Let flags be ? ToString(? Get(R, "flags")). - auto flags_attr = TRY(regexp_object->get(vm.names.flags)); + static Bytecode::PropertyLookupCache cache2; + auto flags_attr = TRY(regexp_object->get(vm.names.flags, cache2)); auto flags = TRY(flags_attr.to_string(vm)); // 5. Let result be the string-concatenation of "/", pattern, "/", and flags. diff --git a/Libraries/LibJS/Runtime/StringPrototype.cpp b/Libraries/LibJS/Runtime/StringPrototype.cpp index 11b83f9a772..e48a14d36f6 100644 --- a/Libraries/LibJS/Runtime/StringPrototype.cpp +++ b/Libraries/LibJS/Runtime/StringPrototype.cpp @@ -587,7 +587,8 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::match) auto regexp = vm.argument(0); if (regexp.is_object()) { // a. Let matcher be ? GetMethod(regexp, @@match). - auto matcher = TRY(regexp.get_method(vm, vm.well_known_symbol_match())); + static Bytecode::PropertyLookupCache cache; + auto matcher = TRY(regexp.get_method(vm, vm.well_known_symbol_match(), cache)); // b. If matcher is not undefined, then if (matcher) { @@ -634,7 +635,8 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::match_all) } // c. Let matcher be ? GetMethod(regexp, @@matchAll). - auto matcher = TRY(regexp.get_method(vm, vm.well_known_symbol_match_all())); + static Bytecode::PropertyLookupCache cache; + auto matcher = TRY(regexp.get_method(vm, vm.well_known_symbol_match_all(), cache)); // d. If matcher is not undefined, then if (matcher) { @@ -804,7 +806,8 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace) // 2. If searchValue is an Object, then if (search_value.is_object()) { // a. Let replacer be ? GetMethod(searchValue, @@replace). - auto replacer = TRY(search_value.get_method(vm, vm.well_known_symbol_replace())); + static Bytecode::PropertyLookupCache cache; + auto replacer = TRY(search_value.get_method(vm, vm.well_known_symbol_replace(), cache)); // b. If replacer is not undefined, then if (replacer) { @@ -899,7 +902,8 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all) } // c. Let replacer be ? GetMethod(searchValue, @@replace). - auto replacer = TRY(search_value.get_method(vm, vm.well_known_symbol_replace())); + static Bytecode::PropertyLookupCache cache; + auto replacer = TRY(search_value.get_method(vm, vm.well_known_symbol_replace(), cache)); // d. If replacer is not undefined, then if (replacer) { @@ -999,7 +1003,8 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::search) // 2. If regexp is an Object, then if (regexp.is_object()) { // a. Let searcher be ? GetMethod(regexp, @@search). - auto searcher = TRY(regexp.get_method(vm, vm.well_known_symbol_search())); + static Bytecode::PropertyLookupCache cache; + auto searcher = TRY(regexp.get_method(vm, vm.well_known_symbol_search(), cache)); // b. If searcher is not undefined, then if (searcher) { @@ -1080,7 +1085,8 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::split) // 2. If separator is an Object, then if (separator_argument.is_object()) { // a. Let splitter be ? GetMethod(separator, @@split). - auto splitter = TRY(separator_argument.get_method(vm, vm.well_known_symbol_split())); + static Bytecode::PropertyLookupCache cache; + auto splitter = TRY(separator_argument.get_method(vm, vm.well_known_symbol_split(), cache)); // b. If splitter is not undefined, then if (splitter) { // i. Return ? Call(splitter, separator, ยซ O, limit ยป). diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index a5bd14ec6a0..81e21de4069 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2020-2025, Andreas Kling * Copyright (c) 2020-2023, Linus Groh * Copyright (c) 2022, David Tuin * @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -304,7 +305,8 @@ ThrowCompletionOr Value::is_regexp(VM& vm) const return false; // 2. Let matcher be ? Get(argument, @@match). - auto matcher = TRY(as_object().get(vm.well_known_symbol_match())); + static Bytecode::PropertyLookupCache cache; + auto matcher = TRY(as_object().get(vm.well_known_symbol_match(), cache)); // 3. If matcher is not undefined, return ToBoolean(matcher). if (!matcher.is_undefined()) @@ -568,7 +570,8 @@ ThrowCompletionOr Value::to_primitive_slow_case(VM& vm, PreferredType pre // 1. If input is an Object, then if (is_object()) { // a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive). - auto exotic_to_primitive = TRY(get_method(vm, vm.well_known_symbol_to_primitive())); + static Bytecode::PropertyLookupCache cache; + auto exotic_to_primitive = TRY(get_method(vm, vm.well_known_symbol_to_primitive(), cache)); // b. If exoticToPrim is not undefined, then if (exotic_to_primitive) { @@ -1306,6 +1309,13 @@ ThrowCompletionOr Value::get(VM& vm, PropertyKey const& property_key) con return TRY(object->internal_get(property_key, *this)); } +ThrowCompletionOr Value::get(VM& vm, PropertyKey const& property, Bytecode::PropertyLookupCache& cache) const +{ + if (is_nullish()) + return vm.throw_completion(ErrorType::ToObjectNullOrUndefined); + return Bytecode::get_by_id(vm, [&]() { return Optional {}; }, [&]() { return property; }, *this, *this, cache); +} + // 7.3.11 GetMethod ( V, P ), https://tc39.es/ecma262/#sec-getmethod ThrowCompletionOr> Value::get_method(VM& vm, PropertyKey const& property_key) const { @@ -1324,6 +1334,24 @@ ThrowCompletionOr> Value::get_method(VM& vm, PropertyKey return function.as_function(); } +// 7.3.11 GetMethod ( V, P ), https://tc39.es/ecma262/#sec-getmethod +ThrowCompletionOr> Value::get_method(VM& vm, PropertyKey const& property_key, Bytecode::PropertyLookupCache& cache) const +{ + // 1. Let func be ? GetV(V, P). + auto function = TRY(get(vm, property_key, cache)); + + // 2. If func is either undefined or null, return undefined. + if (function.is_nullish()) + return nullptr; + + // 3. If IsCallable(func) is false, throw a TypeError exception. + if (!function.is_function()) + return vm.throw_completion(ErrorType::NotAFunction, function.to_string_without_side_effects()); + + // 4. Return func. + return function.as_function(); +} + // 13.10 Relational Operators, https://tc39.es/ecma262/#sec-relational-operators // RelationalExpression : RelationalExpression > ShiftExpression ThrowCompletionOr greater_than(VM& vm, Value lhs, Value rhs) @@ -2168,7 +2196,8 @@ ThrowCompletionOr instance_of(VM& vm, Value value, Value target) return vm.throw_completion(ErrorType::NotAnObject, target.to_string_without_side_effects()); // 2. Let instOfHandler be ? GetMethod(target, @@hasInstance). - auto instance_of_handler = TRY(target.get_method(vm, vm.well_known_symbol_has_instance())); + static Bytecode::PropertyLookupCache cache; + auto instance_of_handler = TRY(target.get_method(vm, vm.well_known_symbol_has_instance(), cache)); // 3. If instOfHandler is not undefined, then if (instance_of_handler) { @@ -2215,7 +2244,8 @@ ThrowCompletionOr ordinary_has_instance(VM& vm, Value lhs, Value rhs) auto* lhs_object = &lhs.as_object(); // 4. Let P be ? Get(C, "prototype"). - auto rhs_prototype = TRY(rhs_function.get(vm.names.prototype)); + static Bytecode::PropertyLookupCache cache; + auto rhs_prototype = TRY(rhs.get(vm, vm.names.prototype, cache)); // 5. If P is not an Object, throw a TypeError exception. if (!rhs_prototype.is_object()) diff --git a/Libraries/LibJS/Runtime/Value.h b/Libraries/LibJS/Runtime/Value.h index cc5588a3f5c..12540080807 100644 --- a/Libraries/LibJS/Runtime/Value.h +++ b/Libraries/LibJS/Runtime/Value.h @@ -382,7 +382,10 @@ public: bool to_boolean() const; ThrowCompletionOr get(VM&, PropertyKey const&) const; + ThrowCompletionOr get(VM&, PropertyKey const&, Bytecode::PropertyLookupCache&) const; + ThrowCompletionOr> get_method(VM&, PropertyKey const&) const; + ThrowCompletionOr> get_method(VM&, PropertyKey const&, Bytecode::PropertyLookupCache&) const; [[nodiscard]] String to_string_without_side_effects() const; [[nodiscard]] Utf16String to_utf16_string_without_side_effects() const;