/* * 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 #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 COLD 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)) [[likely]] 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_array_like_size() }; } } auto& shape = base_obj->shape(); for (auto& cache_entry : cache.entries) { auto cached_prototype = cache_entry.prototype.ptr(); if (cached_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 (shape.is_dictionary()) { if (shape.dictionary_generation() != cache_entry.shape_dictionary_generation) [[unlikely]] { return false; } } auto cached_prototype_chain_validity = cache_entry.prototype_chain_validity.ptr(); if (!cached_prototype_chain_validity) [[unlikely]] return false; if (!cached_prototype_chain_validity->is_valid()) [[unlikely]] return false; return true; }(); if (can_use_cache) [[likely]] { auto value = cached_prototype->get_direct(cache_entry.property_offset); 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. bool can_use_cache = true; if (shape.is_dictionary()) { if (shape.dictionary_generation() != cache_entry.shape_dictionary_generation) [[unlikely]] { can_use_cache = false; } } if (can_use_cache) [[likely]] { auto value = base_obj->get_direct(cache_entry.property_offset); if (value.is_accessor()) { return TRY(call(vm, value.as_accessor().getter(), this_value)); } return value; } } } GC::Ptr prototype_chain_validity; if (shape.prototype()) prototype_chain_validity = shape.prototype()->shape().prototype_chain_validity(); 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(); if (shape.is_dictionary()) { entry.shape_dictionary_generation = shape.dictionary_generation(); } } 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 = const_cast(cacheable_metadata.prototype.ptr()); entry.prototype_chain_validity = prototype_chain_validity; if (shape.is_dictionary()) { entry.shape_dictionary_generation = shape.dictionary_generation(); } } } return value; } template COLD Completion throw_null_or_undefined_property_access(VM& vm, Value base_value, BaseType const& base_identifier, PropertyType const& property_identifier) { VERIFY(base_value.is_nullish()); bool has_base_identifier = true; bool has_property_identifier = true; if constexpr (requires { base_identifier.has_value(); }) has_base_identifier = base_identifier.has_value(); if constexpr (requires { property_identifier.has_value(); }) has_property_identifier = property_identifier.has_value(); if (has_base_identifier && has_property_identifier) return vm.throw_completion(ErrorType::ToObjectNullOrUndefinedWithPropertyAndName, property_identifier, base_value, base_identifier); if (has_property_identifier) return vm.throw_completion(ErrorType::ToObjectNullOrUndefinedWithProperty, property_identifier, base_value); if (has_base_identifier) return vm.throw_completion(ErrorType::ToObjectNullOrUndefinedWithName, base_identifier, base_value); return vm.throw_completion(ErrorType::ToObjectNullOrUndefined); } inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value this_value, Value value, Optional const base_identifier, PropertyKey const& name, PutKind kind, Strict strict, PropertyLookupCache* caches = nullptr) { // Better error message than to_object would give if (strict == Strict::Yes && base.is_nullish()) [[unlikely]] return vm.throw_completion(ErrorType::ReferenceNullishSetProperty, name, base); // a. Let baseObj be ? ToObject(V.[[Base]]). auto maybe_object = base.to_object(vm); if (maybe_object.is_error()) [[unlikely]] return throw_null_or_undefined_property_access(vm, base, base_identifier, name); auto object = maybe_object.release_value(); if (kind == PutKind::Getter || kind == PutKind::Setter) { // The generator should only pass us functions for getters and setters. VERIFY(value.is_function()); } switch (kind) { case PutKind::Getter: { auto& function = value.as_function(); if (is(function) && static_cast(function).name().is_empty()) static_cast(&function)->set_name(Utf16String::formatted("get {}", name)); object->define_direct_accessor(name, &function, nullptr, Attribute::Configurable | Attribute::Enumerable); break; } case PutKind::Setter: { auto& function = value.as_function(); if (is(function) && static_cast(function).name().is_empty()) static_cast(&function)->set_name(Utf16String::formatted("set {}", name)); object->define_direct_accessor(name, nullptr, &function, Attribute::Configurable | Attribute::Enumerable); break; } case PutKind::Normal: { auto this_value_object = MUST(this_value.to_object(vm)); auto& from_shape = this_value_object->shape(); if (caches) [[likely]] { for (size_t i = 0; i < caches->entries.size(); ++i) { switch (caches->types[i]) { case PropertyLookupCache::Entry::Type::Empty: break; case PropertyLookupCache::Entry::Type::ChangePropertyInPrototypeChain: { auto& cache = caches->entries[i]; auto cached_prototype = cache.prototype.ptr(); if (!cached_prototype) [[unlikely]] break; auto cached_shape = cache.shape.ptr(); // 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 (&object->shape() != cached_shape) [[unlikely]] return false; if (cached_shape->is_dictionary()) { if (object->shape().dictionary_generation() != cache.shape_dictionary_generation) [[unlikely]] return false; } auto cached_prototype_chain_validity = cache.prototype_chain_validity.ptr(); if (!cached_prototype_chain_validity) [[unlikely]] return false; if (!cached_prototype_chain_validity->is_valid()) [[unlikely]] return false; return true; }(); if (can_use_cache) [[likely]] { auto value_in_prototype = cached_prototype->get_direct(cache.property_offset); if (value_in_prototype.is_accessor()) [[unlikely]] { (void)TRY(call(vm, value_in_prototype.as_accessor().setter(), this_value, value)); return {}; } } break; } case PropertyLookupCache::Entry::Type::ChangeOwnProperty: { auto& cache = caches->entries[i]; auto cached_shape = cache.shape.ptr(); if (cached_shape != &object->shape()) [[unlikely]] break; if (cached_shape->is_dictionary()) { if (cached_shape->dictionary_generation() != cache.shape_dictionary_generation) break; } auto value_in_object = object->get_direct(cache.property_offset); if (value_in_object.is_accessor()) [[unlikely]] { (void)TRY(call(vm, value_in_object.as_accessor().setter(), this_value, value)); } else { object->put_direct(cache.property_offset, value); } return {}; } case PropertyLookupCache::Entry::Type::AddOwnProperty: { auto& cache = caches->entries[i]; // OPTIMIZATION: If the object's shape is the same as the one cached before adding the new property, we can // reuse the resulting shape from the cache. if (cache.from_shape != &object->shape()) [[unlikely]] break; auto cached_shape = cache.shape.ptr(); if (!cached_shape) [[unlikely]] break; // Cannot add properties to non-extensible objects (frozen, sealed, or preventExtensions). if (!TRY(object->internal_is_extensible())) [[unlikely]] break; if (cached_shape->is_dictionary()) { if (object->shape().dictionary_generation() != cache.shape_dictionary_generation) break; } // The cache is invalid if the prototype chain has been mutated, since such a mutation could have added a setter for the property. auto cached_prototype_chain_validity = cache.prototype_chain_validity.ptr(); if (cached_prototype_chain_validity && !cached_prototype_chain_validity->is_valid()) [[unlikely]] break; object->unsafe_set_shape(*cached_shape); object->put_direct(cache.property_offset, value); return {}; } default: VERIFY_NOT_REACHED(); } } } CacheableSetPropertyMetadata cacheable_metadata; bool succeeded = TRY(object->internal_set(name, value, this_value, &cacheable_metadata)); if (succeeded && caches && cacheable_metadata.type == CacheableSetPropertyMetadata::Type::AddOwnProperty) { caches->update(PropertyLookupCache::Entry::Type::AddOwnProperty, [&](auto& cache) { cache.from_shape = from_shape; cache.property_offset = cacheable_metadata.property_offset.value(); cache.shape = &object->shape(); if (cacheable_metadata.prototype) { cache.prototype_chain_validity = cacheable_metadata.prototype->shape().prototype_chain_validity(); } if (object->shape().is_dictionary()) { cache.shape_dictionary_generation = object->shape().dictionary_generation(); } }); } // If internal_set() caused object's shape change, we can no longer be sure // that collected metadata is valid, e.g. if setter in prototype chain added // property with the same name into the object itself. if (succeeded && caches && &from_shape == &object->shape()) { switch (cacheable_metadata.type) { case CacheableSetPropertyMetadata::Type::AddOwnProperty: // Something went wrong if we ended up here, because cacheable addition of a new property should've changed the shape. VERIFY_NOT_REACHED(); break; case CacheableSetPropertyMetadata::Type::ChangeOwnProperty: caches->update(PropertyLookupCache::Entry::Type::ChangeOwnProperty, [&](auto& cache) { cache.shape = &object->shape(); cache.property_offset = cacheable_metadata.property_offset.value(); if (object->shape().is_dictionary()) { cache.shape_dictionary_generation = object->shape().dictionary_generation(); } }); break; case CacheableSetPropertyMetadata::Type::ChangePropertyInPrototypeChain: caches->update(PropertyLookupCache::Entry::Type::ChangePropertyInPrototypeChain, [&](auto& cache) { cache.shape = &object->shape(); cache.property_offset = cacheable_metadata.property_offset.value(); cache.prototype = const_cast(cacheable_metadata.prototype.ptr()); cache.prototype_chain_validity = cacheable_metadata.prototype->shape().prototype_chain_validity(); if (object->shape().is_dictionary()) { cache.shape_dictionary_generation = object->shape().dictionary_generation(); } }); break; case CacheableSetPropertyMetadata::Type::NotCacheable: break; default: VERIFY_NOT_REACHED(); } } if (!succeeded && strict == Strict::Yes) [[unlikely]] { if (base.is_object()) return vm.throw_completion(ErrorType::ReferenceNullishSetProperty, name, base); return vm.throw_completion(ErrorType::ReferencePrimitiveSetProperty, name, base.typeof_(vm)->utf8_string(), base); } break; } case PutKind::Own: { if (caches) [[likely]] { for (size_t i = 0; i < caches->entries.size(); ++i) { if (caches->types[i] == PropertyLookupCache::Entry::Type::AddOwnProperty) { auto& cache = caches->entries[i]; if (cache.from_shape != &object->shape()) [[unlikely]] continue; auto cached_shape = cache.shape.ptr(); if (!cached_shape) [[unlikely]] continue; if (cached_shape->is_dictionary()) { if (object->shape().dictionary_generation() != cache.shape_dictionary_generation) continue; } object->unsafe_set_shape(*cached_shape); object->put_direct(cache.property_offset, value); return {}; } } } auto& from_shape = object->shape(); object->define_direct_property(name, value, Attribute::Enumerable | Attribute::Writable | Attribute::Configurable); if (caches && &from_shape != &object->shape()) { caches->update(PropertyLookupCache::Entry::Type::AddOwnProperty, [&](auto& cache) { cache.from_shape = from_shape; cache.shape = &object->shape(); cache.property_offset = object->shape().lookup(name)->offset; if (object->shape().is_dictionary()) { cache.shape_dictionary_generation = object->shape().dictionary_generation(); } }); } break; } case PutKind::Prototype: if (value.is_object() || value.is_null()) [[likely]] MUST(object->internal_set_prototype_of(value.is_object() ? &value.as_object() : nullptr)); break; } return {}; } }