mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-05-05 06:32:30 +02:00
LibJS: Add AST-free construct_class()
Add a standalone construct_class() function that builds a class from a ClassBlueprint and an Executable, replacing the virtual dispatch through ClassElement::class_element_evaluation() with a direct switch on ClassElementDescriptor::Kind. This function reads pre-compiled SharedFunctionInstanceData indices from the blueprint, creates ECMAScriptFunctionObjects at runtime, and handles all class element types: methods, getters, setters, fields (with initializers), and static initializers. The function exists but is not yet called. No behavioral change.
This commit is contained in:
committed by
Andreas Kling
parent
6decb93dd7
commit
fa6a3f31dc
Notes:
github-actions[bot]
2026-02-11 23:01:28 +00:00
Author: https://github.com/awesomekling Commit: https://github.com/LadybirdBrowser/ladybird/commit/fa6a3f31dcb Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7896
304
Libraries/LibJS/Runtime/ClassConstruction.cpp
Normal file
304
Libraries/LibJS/Runtime/ClassConstruction.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Andreas Kling <andreas@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <LibGC/ConservativeVector.h>
|
||||
#include <LibJS/Bytecode/Executable.h>
|
||||
#include <LibJS/Runtime/AbstractOperations.h>
|
||||
#include <LibJS/Runtime/Accessor.h>
|
||||
#include <LibJS/Runtime/ClassConstruction.h>
|
||||
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
|
||||
#include <LibJS/Runtime/Error.h>
|
||||
#include <LibJS/Runtime/Object.h>
|
||||
#include <LibJS/Runtime/PrivateEnvironment.h>
|
||||
#include <LibJS/Runtime/SharedFunctionInstanceData.h>
|
||||
#include <LibJS/Runtime/ValueInlines.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
static void update_function_name(Value value, Utf16FlyString const& name)
|
||||
{
|
||||
if (!value.is_function())
|
||||
return;
|
||||
auto& function = value.as_function();
|
||||
if (is<ECMAScriptFunctionObject>(function) && static_cast<ECMAScriptFunctionObject const&>(function).name().is_empty())
|
||||
static_cast<ECMAScriptFunctionObject&>(function).set_name(name);
|
||||
}
|
||||
|
||||
static ThrowCompletionOr<ClassElementName> resolve_element_key(VM& vm, Bytecode::ClassElementDescriptor const& descriptor, Value property_key)
|
||||
{
|
||||
if (descriptor.is_private) {
|
||||
auto private_environment = vm.running_execution_context().private_environment;
|
||||
VERIFY(private_environment);
|
||||
return ClassElementName { private_environment->resolve_private_identifier(*descriptor.private_identifier) };
|
||||
}
|
||||
|
||||
VERIFY(!property_key.is_special_empty_value());
|
||||
|
||||
if (property_key.is_object())
|
||||
property_key = TRY(property_key.to_primitive(vm, Value::PreferredType::String));
|
||||
|
||||
auto key = TRY(PropertyKey::from_value(vm, property_key));
|
||||
return ClassElementName { key };
|
||||
}
|
||||
|
||||
static Utf16String compute_element_name(ClassElementName const& element_name, StringView prefix = {})
|
||||
{
|
||||
auto name = element_name.visit(
|
||||
[&](PropertyKey const& property_key) {
|
||||
if (property_key.is_symbol()) {
|
||||
auto description = property_key.as_symbol()->description();
|
||||
if (!description.has_value() || description->is_empty())
|
||||
return Utf16String {};
|
||||
return Utf16String::formatted("[{}]", *description);
|
||||
}
|
||||
return property_key.to_string();
|
||||
},
|
||||
[&](PrivateName const& private_name) {
|
||||
return private_name.description.to_utf16_string();
|
||||
});
|
||||
|
||||
return Utf16String::formatted("{}{}{}", prefix, prefix.is_empty() ? "" : " ", name);
|
||||
}
|
||||
|
||||
ThrowCompletionOr<ECMAScriptFunctionObject*> construct_class(
|
||||
VM& vm,
|
||||
Bytecode::ClassBlueprint const& blueprint,
|
||||
Bytecode::Executable const& executable,
|
||||
Environment* class_environment,
|
||||
Environment* outer_environment,
|
||||
Value super_class,
|
||||
ReadonlySpan<Value> element_keys,
|
||||
Optional<Utf16FlyString> const& binding_name,
|
||||
Utf16FlyString const& class_name)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// We might not set the lexical environment but we always want to restore it eventually.
|
||||
ArmedScopeGuard restore_environment = [&] {
|
||||
vm.running_execution_context().lexical_environment = outer_environment;
|
||||
};
|
||||
|
||||
vm.running_execution_context().lexical_environment = class_environment;
|
||||
|
||||
auto proto_parent = GC::Ptr { realm.intrinsics().object_prototype() };
|
||||
auto constructor_parent = realm.intrinsics().function_prototype();
|
||||
|
||||
if (blueprint.has_super_class) {
|
||||
if (super_class.is_null()) {
|
||||
proto_parent = nullptr;
|
||||
} else if (!super_class.is_constructor()) {
|
||||
return vm.throw_completion<TypeError>(ErrorType::ClassExtendsValueNotAConstructorOrNull, super_class);
|
||||
} else {
|
||||
auto super_class_prototype = TRY(super_class.get(vm, vm.names.prototype));
|
||||
if (!super_class_prototype.is_null() && !super_class_prototype.is_object())
|
||||
return vm.throw_completion<TypeError>(ErrorType::ClassExtendsValueInvalidPrototype, super_class_prototype);
|
||||
|
||||
if (super_class_prototype.is_null())
|
||||
proto_parent = nullptr;
|
||||
else
|
||||
proto_parent = super_class_prototype.as_object();
|
||||
|
||||
constructor_parent = super_class.as_object();
|
||||
}
|
||||
}
|
||||
|
||||
auto prototype = Object::create_prototype(realm, proto_parent);
|
||||
|
||||
// FIXME: Step 14.a is done in the parser. By using a synthetic super(...args) which does not call @@iterator of %Array.prototype%
|
||||
auto constructor_shared_data = executable.shared_function_data[blueprint.constructor_shared_function_data_index];
|
||||
auto class_constructor = ECMAScriptFunctionObject::create_from_function_data(
|
||||
realm,
|
||||
*constructor_shared_data,
|
||||
vm.lexical_environment(),
|
||||
vm.running_execution_context().private_environment);
|
||||
|
||||
class_constructor->set_name(class_name);
|
||||
class_constructor->set_home_object(prototype);
|
||||
class_constructor->set_is_class_constructor();
|
||||
class_constructor->define_direct_property(vm.names.prototype, prototype, Attribute::Writable);
|
||||
TRY(class_constructor->internal_set_prototype_of(constructor_parent));
|
||||
|
||||
if (blueprint.has_super_class)
|
||||
class_constructor->set_constructor_kind(ConstructorKind::Derived);
|
||||
|
||||
prototype->define_direct_property(vm.names.constructor, class_constructor, Attribute::Writable | Attribute::Configurable);
|
||||
|
||||
using StaticElement = Variant<ClassFieldDefinition, GC::Ref<ECMAScriptFunctionObject>>;
|
||||
|
||||
GC::ConservativeVector<PrivateElement> static_private_methods(vm.heap());
|
||||
GC::ConservativeVector<PrivateElement> instance_private_methods(vm.heap());
|
||||
GC::ConservativeVector<ClassFieldDefinition> instance_fields(vm.heap());
|
||||
GC::ConservativeVector<StaticElement> static_elements(vm.heap());
|
||||
|
||||
for (size_t element_index = 0; element_index < blueprint.elements.size(); ++element_index) {
|
||||
auto const& descriptor = blueprint.elements[element_index];
|
||||
auto& home_object = descriptor.is_static ? static_cast<Object&>(*class_constructor) : static_cast<Object&>(*prototype);
|
||||
|
||||
switch (descriptor.kind) {
|
||||
case Bytecode::ClassElementDescriptor::Kind::Method:
|
||||
case Bytecode::ClassElementDescriptor::Kind::Getter:
|
||||
case Bytecode::ClassElementDescriptor::Kind::Setter: {
|
||||
auto element_name = TRY(resolve_element_key(vm, descriptor, element_keys[element_index]));
|
||||
|
||||
auto shared_data = executable.shared_function_data[*descriptor.shared_function_data_index];
|
||||
auto& method_function = *ECMAScriptFunctionObject::create_from_function_data(
|
||||
realm,
|
||||
*shared_data,
|
||||
vm.lexical_environment(),
|
||||
vm.running_execution_context().private_environment);
|
||||
|
||||
auto method_value = Value(&method_function);
|
||||
method_function.make_method(home_object);
|
||||
|
||||
if (element_name.has<PropertyKey>()) {
|
||||
auto& property_key = element_name.get<PropertyKey>();
|
||||
switch (descriptor.kind) {
|
||||
case Bytecode::ClassElementDescriptor::Kind::Method: {
|
||||
update_function_name(method_value, compute_element_name(element_name));
|
||||
PropertyDescriptor property_descriptor { .value = method_value, .writable = true, .enumerable = false, .configurable = true };
|
||||
TRY(home_object.define_property_or_throw(property_key, property_descriptor));
|
||||
break;
|
||||
}
|
||||
case Bytecode::ClassElementDescriptor::Kind::Getter: {
|
||||
update_function_name(method_value, compute_element_name(element_name, "get"sv));
|
||||
PropertyDescriptor property_descriptor { .get = &method_function, .enumerable = false, .configurable = true };
|
||||
TRY(home_object.define_property_or_throw(property_key, property_descriptor));
|
||||
break;
|
||||
}
|
||||
case Bytecode::ClassElementDescriptor::Kind::Setter: {
|
||||
update_function_name(method_value, compute_element_name(element_name, "set"sv));
|
||||
PropertyDescriptor property_descriptor { .set = &method_function, .enumerable = false, .configurable = true };
|
||||
TRY(home_object.define_property_or_throw(property_key, property_descriptor));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
} else {
|
||||
auto& private_name = element_name.get<PrivateName>();
|
||||
auto& container = descriptor.is_static ? static_private_methods : instance_private_methods;
|
||||
|
||||
PrivateElement private_element = [&] {
|
||||
switch (descriptor.kind) {
|
||||
case Bytecode::ClassElementDescriptor::Kind::Method:
|
||||
update_function_name(method_value, compute_element_name(element_name));
|
||||
return PrivateElement { private_name, PrivateElement::Kind::Method, method_value };
|
||||
case Bytecode::ClassElementDescriptor::Kind::Getter:
|
||||
update_function_name(method_value, compute_element_name(element_name, "get"sv));
|
||||
return PrivateElement { private_name, PrivateElement::Kind::Accessor, Value(Accessor::create(vm, &method_function, nullptr)) };
|
||||
case Bytecode::ClassElementDescriptor::Kind::Setter:
|
||||
update_function_name(method_value, compute_element_name(element_name, "set"sv));
|
||||
return PrivateElement { private_name, PrivateElement::Kind::Accessor, Value(Accessor::create(vm, nullptr, &method_function)) };
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}();
|
||||
|
||||
// Merge accessor pairs.
|
||||
auto added_to_existing = false;
|
||||
for (auto& existing : container) {
|
||||
if (existing.key == private_element.key) {
|
||||
VERIFY(existing.kind == PrivateElement::Kind::Accessor);
|
||||
VERIFY(private_element.kind == PrivateElement::Kind::Accessor);
|
||||
auto& accessor = private_element.value.as_accessor();
|
||||
if (!accessor.getter())
|
||||
existing.value.as_accessor().set_setter(accessor.setter());
|
||||
else
|
||||
existing.value.as_accessor().set_getter(accessor.getter());
|
||||
added_to_existing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!added_to_existing)
|
||||
container.append(move(private_element));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Bytecode::ClassElementDescriptor::Kind::Field: {
|
||||
auto element_name = TRY(resolve_element_key(vm, descriptor, element_keys[element_index]));
|
||||
|
||||
Variant<GC::Ref<ECMAScriptFunctionObject>, Value, Empty> initializer;
|
||||
if (descriptor.has_initializer) {
|
||||
auto shared_data = executable.shared_function_data[*descriptor.shared_function_data_index];
|
||||
|
||||
// Set class_field_initializer_name at runtime for computed keys.
|
||||
if (!descriptor.is_private && !shared_data->m_class_field_initializer_name.has<PropertyKey>()
|
||||
&& !shared_data->m_class_field_initializer_name.has<PrivateName>()) {
|
||||
shared_data->m_class_field_initializer_name = element_name.visit(
|
||||
[](PropertyKey const& key) -> Variant<PropertyKey, PrivateName, Empty> { return key; },
|
||||
[](PrivateName const& name) -> Variant<PropertyKey, PrivateName, Empty> { return name; });
|
||||
}
|
||||
|
||||
auto function = ECMAScriptFunctionObject::create_from_function_data(
|
||||
realm,
|
||||
*shared_data,
|
||||
vm.lexical_environment(),
|
||||
vm.running_execution_context().private_environment);
|
||||
function->make_method(home_object);
|
||||
initializer = function;
|
||||
}
|
||||
|
||||
ClassFieldDefinition field {
|
||||
move(element_name),
|
||||
move(initializer),
|
||||
};
|
||||
|
||||
if (descriptor.is_static)
|
||||
static_elements.append(move(field));
|
||||
else
|
||||
instance_fields.append(move(field));
|
||||
break;
|
||||
}
|
||||
|
||||
case Bytecode::ClassElementDescriptor::Kind::StaticInitializer: {
|
||||
auto shared_data = executable.shared_function_data[*descriptor.shared_function_data_index];
|
||||
auto body_function = ECMAScriptFunctionObject::create_from_function_data(
|
||||
realm,
|
||||
*shared_data,
|
||||
vm.lexical_environment(),
|
||||
vm.running_execution_context().private_environment);
|
||||
body_function->make_method(home_object);
|
||||
static_elements.append(GC::Ref { *body_function });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.running_execution_context().lexical_environment = outer_environment;
|
||||
restore_environment.disarm();
|
||||
|
||||
if (binding_name.has_value())
|
||||
MUST(class_environment->initialize_binding(vm, binding_name.value(), class_constructor, Environment::InitializeBindingHint::Normal));
|
||||
|
||||
for (auto& field : instance_fields)
|
||||
class_constructor->add_field(field);
|
||||
|
||||
for (auto& private_method : instance_private_methods)
|
||||
class_constructor->add_private_method(private_method);
|
||||
|
||||
for (auto& method : static_private_methods)
|
||||
TRY(class_constructor->private_method_or_accessor_add(move(method)));
|
||||
|
||||
for (auto& element : static_elements) {
|
||||
TRY(element.visit(
|
||||
[&](ClassFieldDefinition& field) -> ThrowCompletionOr<void> {
|
||||
return TRY(class_constructor->define_field(field));
|
||||
},
|
||||
[&](GC::Ref<ECMAScriptFunctionObject> static_block_function) -> ThrowCompletionOr<void> {
|
||||
// We discard any value returned here.
|
||||
TRY(call(vm, *static_block_function, class_constructor));
|
||||
return {};
|
||||
}));
|
||||
}
|
||||
|
||||
class_constructor->set_source_text(blueprint.source_text);
|
||||
|
||||
return { class_constructor };
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user