ladybird/Meta/Generators/libweb_bindings/operations.py
Shannon Booth dba62aefc0 LibWeb/Bindings: Port IDL bindings generator to Python
Replace the Lagom C++ bindings generator invocation with the new Python
generator under Meta/Generators/libweb_bindings.

Fold the exposed-interface generation into the same generator entry
point, and keep generated overload metadata using the existing LibIDL
types for now.
2026-06-08 01:11:40 +02:00

555 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
for operation in interface.regular_operations:
if "FIXME" not in operation.extended_attributes:
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}");
"""
)