/* * 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 #include namespace JS::Bytecode { enum class GetByIdMode { Normal, Length, }; ALWAYS_INLINE ThrowCompletionOr get_cached_property_value(VM& vm, Value value, Value this_value) { if (!value.is_accessor()) return value; // https://tc39.es/ecma262/#sec-ordinaryget // If _getter_ is *undefined*, return *undefined*. auto* getter = value.as_accessor().getter(); if (!getter) return js_undefined(); return TRY(call(vm, *getter, this_value)); } 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 const& property_name = get_property_name(); if (base_value.is_string()) { // https://tc39.es/ecma262/#sec-stringgetownproperty // String exotic objects expose virtual own properties for canonical string indexes. auto string_value = TRY(base_value.as_string().get(vm, property_name)); if (string_value.has_value()) return *string_value; } 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()) { if (cache_entry.type != PropertyLookupCache::Entry::Type::GetOwnProperty && cache_entry.type != PropertyLookupCache::Entry::Type::GetPropertyInPrototypeChain) { continue; } 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); return TRY(get_cached_property_value(vm, value, this_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); return TRY(get_cached_property_value(vm, value, this_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(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()) { if (cacheable_metadata.type == CacheableGetPropertyMetadata::Type::GetOwnProperty) { cache.update(PropertyLookupCache::Entry::Type::GetOwnProperty, [&](auto& entry) { 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) { cache.update(PropertyLookupCache::Entry::Type::GetPropertyInPrototypeChain, [&](auto& entry) { 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_inferred_name(Variant { name }, "get"sv); 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_inferred_name(Variant { name }, "set"sv); 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 (auto& cache : caches->entries()) { switch (cache.type) { case PropertyLookupCache::Entry::Type::Empty: break; case PropertyLookupCache::Entry::Type::ChangePropertyInPrototypeChain: { 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]] { auto* setter = value_in_prototype.as_accessor().setter(); if (!setter) break; (void)TRY(call(vm, *setter, this_value, value)); return {}; } } break; } case PropertyLookupCache::Entry::Type::ChangeOwnProperty: { 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]] { auto* setter = value_in_object.as_accessor().setter(); if (!setter) break; (void)TRY(call(vm, *setter, this_value, value)); } else { object->put_direct(cache.property_offset, value); } return {}; } case PropertyLookupCache::Entry::Type::AddOwnProperty: { // 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 {}; } case PropertyLookupCache::Entry::Type::GetOwnProperty: case PropertyLookupCache::Entry::Type::GetPropertyInPrototypeChain: break; } } } 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)->utf16_string_view(), base); } break; } case PutKind::Own: { if (caches) [[likely]] { for (auto& cache : caches->entries()) { if (cache.type == PropertyLookupCache::Entry::Type::AddOwnProperty) { 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 {}; } }