ladybird/Meta/Generators/libweb_bindings/operations.py
Andreas Kling 4522340416 LibWeb: Preserve implemented overloads with FIXME siblings
Do not emit an unimplemented prototype property for an operation name
when at least one overload with that name is implemented. The overload
resolver already filters FIXME overloads out of the native overload set,
so defining an unimplemented property afterwards replaces the working
function with undefined.

This lets WebGL2RenderingContext.texImage2D keep its implemented WebGL1
and typed-array overloads even though the PBO-offset overload remains
marked FIXME.
2026-06-09 16:08:44 +02:00

558 lines
21 KiB
Python

# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause
from typing import Optional
from typing import TextIO
from Generators.libweb_bindings import overload_resolution
from Generators.libweb_bindings.arguments import write_operation_parameter_conversions
from Generators.libweb_bindings.attributes import reflected_attribute_name
from Generators.libweb_bindings.context import GenerationContext
from Generators.libweb_bindings.cpp_types import fully_qualified_name_for_interface
from Generators.libweb_bindings.cpp_types import idl_identifier_cpp_name
from Generators.libweb_bindings.cpp_types import idl_implementation_cpp_name
from Generators.libweb_bindings.cpp_types import is_numeric_type
from Generators.libweb_bindings.cpp_types import is_string_type
from Generators.libweb_bindings.extended_attributes import wrap_with_ce_reactions
from Generators.libweb_bindings.extended_attributes import wrap_with_extended_attribute_exposure_checks
from Generators.libweb_bindings.includes import GeneratedIncludes
from Generators.libweb_bindings.to_js_value import to_javascript_value
from Utils.webidl_parser import Attribute
from Utils.webidl_parser import IDLParameterizedType
from Utils.webidl_parser import IDLType
from Utils.webidl_parser import Interface
from Utils.webidl_parser import Operation
# https://webidl.spec.whatwg.org/#js-default-operations
def operation_is_default_to_json(operation: Operation) -> bool:
return (
operation.name == "toJSON"
and "Default" in operation.extended_attributes
and operation.return_type.name == "object"
)
# https://webidl.spec.whatwg.org/#dfn-json-types
def idl_type_is_json_type(idl_type: IDLType, context: GenerationContext) -> bool:
# The JSON types are:
# * nullable types whose inner type is a JSON type,
if idl_type.nullable:
return idl_type_is_json_type(idl_type.without_nullable(), context)
if isinstance(idl_type, IDLParameterizedType):
# * sequence types whose parameterized type is a JSON type,
# * frozen array types whose parameterized type is a JSON type,
if idl_type.name in ("sequence", "FrozenArray"):
return idl_type_is_json_type(idl_type.parameters[0], context)
# * records where all of their values are JSON types,
if idl_type.name == "record":
return idl_type_is_json_type(idl_type.parameters[1], context)
# * interface types that have a toJSON operation declared on themselves or one of their inherited interfaces.
interface = context.interface(idl_type)
if interface is not None:
return any(
operation.name == "toJSON" and "Default" in operation.extended_attributes
for interface_in_chain in context.inheritance_stack(interface)
for operation in interface_in_chain.regular_operations
)
# FIXME: * dictionary types where the types of all members declared on the dictionary and all its inherited dictionaries are JSON types,
# * numeric types,
# * boolean,
# * string types,
# * object,
return (
is_numeric_type(idl_type.name)
or idl_type.name == "boolean"
or is_string_type(idl_type.name)
or idl_type.name == "object"
or context.enumeration(idl_type) is not None
)
def define_the_regular_operations(
out: TextIO,
includes: GeneratedIncludes,
interface: Interface,
unforgeable: bool = False,
) -> None:
for name, operations in overload_resolution.operation_overload_sets(interface).items():
if any("LegacyUnforgeable" in operation.extended_attributes for operation in operations) != unforgeable:
continue
operation = operations[0]
out.write(
wrap_with_extended_attribute_exposure_checks(
includes,
operation.extended_attributes,
f""" object.define_native_function(realm, "{name}"_utf16_fly_string, {idl_identifier_cpp_name(operation)}, {overload_resolution.operation_overload_set_length(operations)}, default_attributes);
""",
)
)
if unforgeable:
return
implemented_operation_names = overload_resolution.operation_overload_sets(interface).keys()
for operation in interface.regular_operations:
if "FIXME" not in operation.extended_attributes:
continue
if operation.name in implemented_operation_names:
continue
out.write(
f""" object.define_direct_property("{operation.name}"_utf16_fly_string, JS::js_undefined(), default_attributes | JS::Attribute::Unimplemented);
"""
)
def define_the_static_operations(out: TextIO, includes: GeneratedIncludes, interface: Interface) -> None:
for name, operations in overload_resolution.operation_overload_sets(interface, static=True).items():
operation = operations[0]
out.write(
wrap_with_extended_attribute_exposure_checks(
includes,
operation.extended_attributes,
f""" object.define_native_function(realm, "{name}"_utf16_fly_string, {idl_identifier_cpp_name(operation)}, {overload_resolution.operation_overload_set_length(operations)}, JS::Attribute::Enumerable | JS::Attribute::Configurable | JS::Attribute::Writable);
""",
)
)
def define_unscopable_members(out: TextIO, includes: GeneratedIncludes, interface: Interface) -> None:
unscopable_names = []
for attribute in interface.regular_attributes:
if "FIXME" not in attribute.extended_attributes and "Unscopable" in attribute.extended_attributes:
unscopable_names.append(attribute.name)
for name, operations in overload_resolution.operation_overload_sets(interface).items():
if all("Unscopable" in operation.extended_attributes for operation in operations):
unscopable_names.append(name)
if not unscopable_names:
return
includes.add("LibJS/Runtime/Object.h")
out.write(
""" auto unscopable_object = JS::Object::create(realm, nullptr);
"""
)
for name in unscopable_names:
out.write(
f""" MUST(unscopable_object->create_data_property("{name}"_utf16_fly_string, JS::Value(true)));
"""
)
out.write(
""" object.define_direct_property(vm.well_known_symbol_unscopables(), unscopable_object, JS::Attribute::Configurable);
"""
)
def define_the_stringifier(
out: TextIO,
includes: GeneratedIncludes,
interface: Interface,
unforgeable: bool = False,
) -> None:
if interface.stringifier is None:
return
extended_attributes = interface.stringifier.extended_attributes
if ("LegacyUnforgeable" in extended_attributes) != unforgeable:
return
out.write(
wrap_with_extended_attribute_exposure_checks(
includes,
extended_attributes,
""" object.define_native_function(realm, "toString"_utf16_fly_string, to_string, 0, default_attributes);
""",
)
)
out.write(
"""
"""
)
def write_regular_operations(
out: TextIO, context: GenerationContext, includes: GeneratedIncludes, interface: Interface
) -> None:
for operations in overload_resolution.operation_overload_sets(interface).values():
if len(operations) > 1:
receiver_class = interface.namespace_class if interface.is_namespace else interface.prototype_class
overload_resolution.write_overload_arbiter(
out, context, includes, interface, operations, receiver_class=receiver_class
)
for overload_index, operation in enumerate(operations):
write_operation(out, context, includes, interface, operation, overload_index)
else:
write_operation(out, context, includes, interface, operations[0])
def write_regular_operations_for_receiver(
out: TextIO,
context: GenerationContext,
includes: GeneratedIncludes,
interface: Interface,
receiver_class: str,
defined_callbacks: Optional[set[str]] = None,
) -> None:
for operations in overload_resolution.operation_overload_sets(interface).values():
callbacks = [
idl_identifier_cpp_name(operation, suffix=overload_index if len(operations) > 1 else None)
for overload_index, operation in enumerate(operations)
]
callbacks.append(idl_identifier_cpp_name(operations[0]))
if defined_callbacks is not None and all(callback in defined_callbacks for callback in callbacks):
continue
if defined_callbacks is not None:
defined_callbacks.update(callbacks)
if len(operations) > 1:
overload_resolution.write_overload_arbiter(
out, context, includes, interface, operations, receiver_class=receiver_class
)
for overload_index, operation in enumerate(operations):
write_operation(out, context, includes, interface, operation, overload_index, receiver_class)
else:
write_operation(out, context, includes, interface, operations[0], receiver_class=receiver_class)
def write_static_operations(
out: TextIO, context: GenerationContext, includes: GeneratedIncludes, interface: Interface
) -> None:
for operations in overload_resolution.operation_overload_sets(interface, static=True).values():
if len(operations) > 1:
overload_resolution.write_overload_arbiter(
out,
context,
includes,
interface,
operations,
receiver_class=interface.constructor_class,
)
for overload_index, operation in enumerate(operations):
write_operation(out, context, includes, interface, operation, overload_index, emit_as_static=True)
else:
write_operation(out, context, includes, interface, operations[0], emit_as_static=True)
def write_stringifier(
out: TextIO,
context: GenerationContext,
includes: GeneratedIncludes,
interface: Interface,
receiver_class: Optional[str] = None,
) -> None:
if receiver_class is None:
receiver_class = interface.prototype_class
if interface.stringifier is None:
return
attribute = interface.stringifier.attribute
stringifier_type = attribute.type if attribute is not None else IDLType("DOMString")
stringifier_cpp_name = idl_implementation_cpp_name(attribute) if attribute is not None else "to_string"
out.write(
f"""JS_DEFINE_NATIVE_FUNCTION({receiver_class}::to_string)
{{
WebIDL::log_trace(vm, "{receiver_class}::to_string");
auto* idl_object = TRY(impl_from(vm));
auto R = TRY(throw_dom_exception_if_needed(vm, [&] {{ return idl_object->{stringifier_cpp_name}(); }}));
return {to_javascript_value(stringifier_type, "R", includes, context)};
}}
"""
)
# https://webidl.spec.whatwg.org/#dfn-create-operation-function
def write_operation(
out: TextIO,
context: GenerationContext,
includes: GeneratedIncludes,
interface: Interface,
operation: Operation,
overload_index: Optional[int] = None,
receiver_class: Optional[str] = None,
emit_as_static: bool = False,
) -> None:
if operation_is_default_to_json(operation):
write_default_to_json_operation(out, context, includes, interface, operation)
return
return_type_is_promise = operation.return_type.name == "Promise"
arguments = ", ".join(idl_identifier_cpp_name(parameter) for parameter in operation.parameters)
callee_arguments = arguments
operation_invokes_as_static = emit_as_static or interface.is_namespace
if operation_invokes_as_static:
callee_arguments = f"vm, {arguments}" if arguments else "vm"
callback_name = idl_identifier_cpp_name(operation, suffix=overload_index)
if interface.is_namespace:
receiver_class = receiver_class or interface.namespace_class
else:
receiver_class = receiver_class or (
interface.constructor_class if emit_as_static else interface.prototype_class
)
out.write(
f"""JS_DEFINE_NATIVE_FUNCTION({receiver_class}::{callback_name})
{{
WebIDL::log_trace(vm, "{receiver_class}::{callback_name}");
[[maybe_unused]] auto& realm = *vm.current_realm();
"""
)
if return_type_is_promise:
includes.add("LibWeb/WebIDL/Promise.h")
out.write(
""" auto steps = [&realm, &vm]() -> JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> {
(void)realm;
"""
)
if not operation_invokes_as_static:
out.write(
f""" [[maybe_unused]] {fully_qualified_name_for_interface(interface)}* idl_object = TRY(impl_from(vm));
"""
)
required_argument_count = overload_resolution.operation_length(operation)
if required_argument_count == 1:
out.write(
f""" if (vm.argument_count() < 1)
return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "{operation.name}");
"""
)
elif required_argument_count > 1:
out.write(
f""" if (vm.argument_count() < {required_argument_count})
return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountMany, "{operation.name}", "{required_argument_count}");
"""
)
write_operation_parameter_conversions(out, operation.parameters, includes, context)
if operation_invokes_as_static:
callee = fully_qualified_name_for_interface(interface)
if interface.is_namespace:
callee = fully_qualified_name_for_interface(interface).partition("::")[0]
out.write(
f""" [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] {{ return {callee}::{idl_implementation_cpp_name(operation)}({callee_arguments}); }}));
"""
)
if return_type_is_promise:
out.write(
f""" return R;
}};
auto maybe_R = steps();
// And then, if an exception E was thrown:
// 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
// 2. Otherwise, end these steps and allow the exception to propagate.
if (maybe_R.is_throw_completion())
return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();
auto R = maybe_R.release_value();
return {to_javascript_value(operation.return_type, "R", includes, context)};
}}
"""
)
else:
out.write(
f""" return {to_javascript_value(operation.return_type, "R", includes, context)};
}}
"""
)
return
if "CEReactions" in operation.extended_attributes:
if return_type_is_promise:
raise RuntimeError(
f"Unsupported promise-returning [CEReactions] operation '{operation.name}' on '{interface.name}'"
)
ce_reactions_steps = wrap_with_ce_reactions(includes, "original_steps()")
out.write(
f""" auto original_steps = [&] {{
return throw_dom_exception_if_needed(vm, [&] {{ return idl_object->{idl_implementation_cpp_name(operation)}({arguments}); }});
}};
[[maybe_unused]] auto R = TRY({ce_reactions_steps});
return {to_javascript_value(operation.return_type, "R", includes, context)};
}}
"""
)
return
if return_type_is_promise:
out.write(
f""" [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] {{ return idl_object->{idl_implementation_cpp_name(operation)}({arguments}); }}));
return R;
}};
auto maybe_R = steps();
// And then, if an exception E was thrown:
// 1. If op has a return type that is a promise type, then return ! Call(%Promise.reject%, %Promise%, «E»).
// 2. Otherwise, end these steps and allow the exception to propagate.
if (maybe_R.is_throw_completion())
return WebIDL::create_rejected_promise(realm, maybe_R.error_value())->promise();
return {to_javascript_value(operation.return_type, "maybe_R.release_value()", includes, context)};
}}
"""
)
return
out.write(
f""" [[maybe_unused]] auto R = TRY(throw_dom_exception_if_needed(vm, [&] {{ return idl_object->{idl_implementation_cpp_name(operation)}({arguments}); }}));
return {to_javascript_value(operation.return_type, "R", includes, context)};
}}
"""
)
# FIXME: This belongs is an attribute getter helper somewhere.
def default_to_json_getter_steps(
attribute: Attribute,
value_name: str,
) -> str:
is_reflected = "Reflect" in attribute.extended_attributes
if is_reflected and attribute.type.name == "boolean":
return f'auto {value_name} = idl_object->has_attribute("{reflected_attribute_name(attribute)}"_fly_string);'
if is_reflected:
return (
f'auto {value_name} = idl_object->get_attribute_value("{reflected_attribute_name(attribute)}"_fly_string);'
)
return f"auto {value_name} = TRY(throw_dom_exception_if_needed(vm, [&] {{ return idl_object->{idl_implementation_cpp_name(attribute)}(); }}));"
# https://webidl.spec.whatwg.org/#collect-attribute-values
def collect_attribute_values(
interface: Interface,
context: GenerationContext,
includes: GeneratedIncludes,
attribute_values: list[str],
) -> None:
# 1. If a toJSON operation with a [Default] extended attribute is declared on I
# then for each exposed regular attribute attr that is an interface member of I, in order:
if not any(operation_is_default_to_json(operation) for operation in interface.regular_operations):
return
for attribute in interface.regular_attributes:
# 1. Let id be the identifier of attr.
# 2. Let value be the result of running the getter steps of attr with object as this.
# 3. If value is a JSON type, then set map[id] to value.
if not idl_type_is_json_type(attribute.type, context):
continue
value_name = f"{idl_identifier_cpp_name(attribute)}_{len(attribute_values)}_value"
key_name = f"{idl_identifier_cpp_name(attribute)}_{len(attribute_values)}_key"
js_value_name = f"{value_name}_js"
getter_steps = default_to_json_getter_steps(attribute, value_name)
attribute_values.append(
wrap_with_extended_attribute_exposure_checks(
includes,
attribute.extended_attributes,
f""" {getter_steps}
// 1. Let k be key converted to a JavaScript value.
auto {key_name} = "{attribute.name}"_utf16_fly_string;
// 2. Let v be value converted to a JavaScript value.
auto {js_value_name} = {to_javascript_value(attribute.type, value_name, includes, context)};
// 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
MUST(result->create_data_property({key_name}, {js_value_name}));
""",
)
)
# https://webidl.spec.whatwg.org/#collect-attribute-values-of-an-inheritance-stack
def collect_attribute_values_of_an_inheritance_stack(
stack: list[Interface],
context: GenerationContext,
includes: GeneratedIncludes,
attribute_values: list[str],
) -> None:
# 1. Let I be the result of popping from stack.
interface = stack.pop()
# 2. Invoke collect attribute values given object, I, and map.
collect_attribute_values(interface, context, includes, attribute_values)
# 3. If stack is not empty, then invoke collect attribute values of an inheritance stack given object, stack, and map.
if stack:
collect_attribute_values_of_an_inheritance_stack(stack, context, includes, attribute_values)
# https://webidl.spec.whatwg.org/#js-default-tojson
def write_default_to_json_operation(
out: TextIO,
context: GenerationContext,
includes: GeneratedIncludes,
interface: Interface,
operation: Operation,
) -> None:
includes.add("LibJS/Runtime/Object.h")
includes.add("LibWeb/WebIDL/Tracing.h")
# 1. Let map be a new ordered map.
# 2. Let stack be the result of creating an inheritance stack for interface I.
stack = context.inheritance_stack(interface)
# 3. Invoke collect attribute values of an inheritance stack given this, stack, and map.
attribute_values: list[str] = []
collect_attribute_values_of_an_inheritance_stack(stack, context, includes, attribute_values)
out.write(
f"""JS_DEFINE_NATIVE_FUNCTION({interface.prototype_class}::{idl_identifier_cpp_name(operation)})
{{
WebIDL::log_trace(vm, "{interface.prototype_class}::{idl_identifier_cpp_name(operation)}");
auto& realm = *vm.current_realm();
[[maybe_unused]] auto* idl_object = TRY(impl_from(vm));
// 4. Let result be OrdinaryObjectCreate(%Object.prototype%).
auto result = JS::Object::create(realm, realm.intrinsics().object_prototype());
// 5. For each key → value of map:
{"".join(attribute_values)}
// 6. Return result.
return result;
}}
"""
)
def write_argument_count_check(out: TextIO, function_name: str, argument_count: int) -> None:
if argument_count == 0:
return
if argument_count == 1:
out.write(
f""" if (vm.argument_count() < 1)
return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountOne, "{function_name}");
"""
)
return
out.write(
f""" if (vm.argument_count() < {argument_count})
return vm.throw_completion<JS::TypeError>(JS::ErrorType::BadArgCountMany, "{function_name}", "{argument_count}");
"""
)