Compare commits

..

3 commits

Author SHA1 Message Date
Andreas Kling
d065171791 LibJS: Use property lookup caches for some of our hot C++ gets
We can use caching in a million more places. This is just me running JS
benchmarks and looking at which get() call sites were hot and putting
caches there.

Lots of nice speedups all over the place, some examples:

1.19x speedup on Octane/raytrace.js
1.13x speedup on Octane/earley-boyer.js
1.12x speedup on Kraken/ai-astar.js
1.10x speedup on Octane/box2d.js
1.08x speedup on Octane/gbemu.js
1.05x speedup on Octane/regexp.js
2025-10-14 15:47:38 +02:00
Andreas Kling
0fb9ba1e3a LibJS: Add Value::get() and Object::get() overloads with lookup cache
To speed up property access, callers of get() can now provide a lookup
cache like so:

    static Bytecode::PropertyLookupCache cache;
    auto value = TRY(object.get(property, cache));

Note that the cache has to be `static` or it won't make sense!

This basically brings the inline caches from our bytecode VM straight
into C++ land, allowing us to gain serious performance improvements.
The implementation shares code with the GetById bytecode instruction.
2025-10-14 15:47:38 +02:00
Andreas Kling
26c1dea22a LibJS: Move GetById logic to a separate header to prepare for reuse
We also make the code a bit more generic by making callers provide
(templated) callbacks that produce the property name and base expression
string if any.
2025-10-14 15:47:38 +02:00
15 changed files with 300 additions and 161 deletions

View file

@ -16,6 +16,7 @@
#include <LibJS/Bytecode/Interpreter.h>
#include <LibJS/Bytecode/Label.h>
#include <LibJS/Bytecode/Op.h>
#include <LibJS/Bytecode/PropertyAccess.h>
#include <LibJS/Export.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Accessor.h>
@ -922,27 +923,6 @@ ALWAYS_INLINE Completion throw_null_or_undefined_property_access(VM& vm, Value b
return vm.throw_completion<TypeError>(ErrorType::ToObjectNullOrUndefined);
}
ALWAYS_INLINE GC::Ptr<Object> 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<GC::Ref<Object>> base_object_for_get(VM& vm, Value base_value, Optional<IdentifierTableIndex> 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<GC::Ref<Object>> 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<GetByIdMode mode = GetByIdMode::Normal>
inline ThrowCompletionOr<Value> get_by_id(VM& vm, Optional<IdentifierTableIndex> 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<PrototypeChainValidity> 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<Value> get_by_value(VM& vm, Optional<IdentifierTableIndex> 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<void> 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<GetByIdMode::Normal>(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<void> 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<GetByIdMode::Normal>(interpreter.vm(), [] { return Optional<Utf16FlyString const&> {}; }, [&] { return interpreter.get_identifier(m_property); }, base_value, this_value, cache)));
return {};
}
@ -2656,7 +2550,7 @@ ThrowCompletionOr<void> 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<GetByIdMode::Length>(interpreter.vm(), m_base_identifier, *executable.length_identifier, base_value, base_value, cache, executable)));
interpreter.set(dst(), TRY(get_by_id<GetByIdMode::Length>(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<void> 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<GetByIdMode::Length>(interpreter.vm(), {}, *executable.length_identifier, base_value, this_value, cache, executable)));
interpreter.set(dst(), TRY(get_by_id<GetByIdMode::Length>(interpreter.vm(), [] { return Optional<Utf16FlyString const&> {}; }, [&] { return executable.get_identifier(*executable.length_identifier); }, base_value, this_value, cache)));
return {};
}

View file

@ -0,0 +1,152 @@
/*
* Copyright (c) 2021-2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Bytecode/IdentifierTable.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Accessor.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/Shape.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWasm/Opcode.h>
namespace JS::Bytecode {
enum class GetByIdMode {
Normal,
Length,
};
ALWAYS_INLINE GC::Ptr<Object> 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<typename GetBaseIdentifier, typename GetPropertyName>
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<TypeError>(ErrorType::ToObjectNullOrUndefinedWithPropertyAndName, get_property_name(), base_value, base_identifier);
return vm.throw_completion<TypeError>(ErrorType::ToObjectNullOrUndefinedWithProperty, get_property_name(), base_value);
}
template<typename GetBaseIdentifier, typename GetPropertyName>
ALWAYS_INLINE ThrowCompletionOr<GC::Ref<Object>> 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<GetByIdMode mode, typename GetBaseIdentifier, typename GetPropertyName>
ALWAYS_INLINE ThrowCompletionOr<Value> 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<PrototypeChainValidity> 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;
}
}

View file

@ -327,6 +327,7 @@ class Generator;
class Instruction;
class Interpreter;
class Operand;
struct PropertyLookupCache;
class RegexTable;
class Register;

View file

@ -127,7 +127,8 @@ ThrowCompletionOr<size_t> 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<GC::RootVector<Value>> create_list_from_array_like(VM& vm, Val
ThrowCompletionOr<FunctionObject*> 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<FunctionObject*> species_constructor(VM& vm, Object const& obj
return vm.throw_completion<TypeError>(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<Object*> 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()) {

View file

@ -119,7 +119,8 @@ static ThrowCompletionOr<Object*> 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<Object*> 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();
}

View file

@ -22,7 +22,8 @@ ThrowCompletionOr<GC::Ref<AsyncGenerator>> 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<AsyncGenerator>(realm, generating_function_prototype_object, move(execution_context));
object->m_generating_function = generating_function;

View file

@ -29,7 +29,8 @@ ThrowCompletionOr<GC::Ref<GeneratorObject>> 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<GeneratorObject>(realm, generating_function_prototype_object, move(execution_context));

View file

@ -39,7 +39,8 @@ Iterator::Iterator(Object& prototype)
ThrowCompletionOr<GC::Ref<IteratorRecord>> 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<GC::Ref<IteratorRecord>> get_iterator_from_method(VM& vm, Valu
return vm.throw_completion<TypeError>(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<IteratorRecord>(iterator.as_object(), next_method, false);
@ -79,7 +81,8 @@ ThrowCompletionOr<GC::Ref<IteratorRecord>> 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<GC::Ref<IteratorRecord>> 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<GC::Ref<IteratorRecord>> 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<GC::Ref<Object>> iterator_next(VM& vm, IteratorRecord& iterato
ThrowCompletionOr<bool> 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<Value> 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<IterationResultOrDone> 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<IterationResultOrDone> iterator_step(VM& vm, IteratorRecord& i
}
// 6. Return result.
return ThrowCompletionOr<IterationResultOrDone> { IterationResult { done_value, result->get(vm.names.value) } };
static Bytecode::PropertyLookupCache cache2;
return ThrowCompletionOr<IterationResultOrDone> { IterationResult { done_value, result->get(vm.names.value, cache2) } };
}
// 7.4.10 IteratorStepValue ( iteratorRecord ), https://tc39.es/ecma262/#sec-iteratorstepvalue

View file

@ -125,6 +125,13 @@ ThrowCompletionOr<Value> 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<Value> 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<Value> 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()) {

View file

@ -121,6 +121,7 @@ public:
// 7.3 Operations on Objects, https://tc39.es/ecma262/#sec-operations-on-objects
ThrowCompletionOr<Value> get(PropertyKey const&) const;
ThrowCompletionOr<Value> get(PropertyKey const&, Bytecode::PropertyLookupCache&) const;
ThrowCompletionOr<void> set(PropertyKey const&, Value, ShouldThrowExceptions);
ThrowCompletionOr<bool> create_data_property(PropertyKey const&, Value, Optional<u32>* new_property_offset = nullptr);
void create_method_property(PropertyKey const&, Value);

View file

@ -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;

View file

@ -61,7 +61,8 @@ void RegExpPrototype::initialize(Realm& realm)
static ThrowCompletionOr<void> 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<Value> 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<Value> regexp_builtin_exec(VM& vm, RegExpObject& regexp
ThrowCompletionOr<Value> regexp_exec(VM& vm, Object& regexp_object, GC::Ref<PrimitiveString> 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.

View file

@ -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 »).

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, David Tuin <davidot@serenityos.org>
*
@ -15,6 +15,7 @@
#include <AK/Utf16String.h>
#include <AK/Utf8View.h>
#include <LibCrypto/BigInt/SignedBigInteger.h>
#include <LibJS/Bytecode/PropertyAccess.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Accessor.h>
#include <LibJS/Runtime/Array.h>
@ -304,7 +305,8 @@ ThrowCompletionOr<bool> 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> 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> Value::get(VM& vm, PropertyKey const& property_key) con
return TRY(object->internal_get(property_key, *this));
}
ThrowCompletionOr<Value> Value::get(VM& vm, PropertyKey const& property, Bytecode::PropertyLookupCache& cache) const
{
if (is_nullish())
return vm.throw_completion<TypeError>(ErrorType::ToObjectNullOrUndefined);
return Bytecode::get_by_id<Bytecode::GetByIdMode::Normal>(vm, [&]() { return Optional<Utf16FlyString const&> {}; }, [&]() { return property; }, *this, *this, cache);
}
// 7.3.11 GetMethod ( V, P ), https://tc39.es/ecma262/#sec-getmethod
ThrowCompletionOr<GC::Ptr<FunctionObject>> Value::get_method(VM& vm, PropertyKey const& property_key) const
{
@ -1324,6 +1334,24 @@ ThrowCompletionOr<GC::Ptr<FunctionObject>> Value::get_method(VM& vm, PropertyKey
return function.as_function();
}
// 7.3.11 GetMethod ( V, P ), https://tc39.es/ecma262/#sec-getmethod
ThrowCompletionOr<GC::Ptr<FunctionObject>> 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<TypeError>(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<bool> greater_than(VM& vm, Value lhs, Value rhs)
@ -2168,7 +2196,8 @@ ThrowCompletionOr<Value> instance_of(VM& vm, Value value, Value target)
return vm.throw_completion<TypeError>(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<Value> 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())

View file

@ -382,7 +382,10 @@ public:
bool to_boolean() const;
ThrowCompletionOr<Value> get(VM&, PropertyKey const&) const;
ThrowCompletionOr<Value> get(VM&, PropertyKey const&, Bytecode::PropertyLookupCache&) const;
ThrowCompletionOr<GC::Ptr<FunctionObject>> get_method(VM&, PropertyKey const&) const;
ThrowCompletionOr<GC::Ptr<FunctionObject>> 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;