diff --git a/Meta/Generators/generate_window_or_worker_interfaces.py b/Meta/Generators/generate_window_or_worker_interfaces.py index 38815bf89f..859c098be5 100644 --- a/Meta/Generators/generate_window_or_worker_interfaces.py +++ b/Meta/Generators/generate_window_or_worker_interfaces.py @@ -471,7 +471,7 @@ WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototy ) named_properties_class = "" - if "Global" in interface.extended_attributes and interface.supports_named_properties(): + if "Global" in interface.extended_attributes and interface.supports_named_properties: named_properties_class = f"{interface.name}Properties" if named_properties_class: @@ -505,7 +505,7 @@ WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototy return named_properties_class = "" - if "Global" in interface.extended_attributes and interface.supports_named_properties(): + if "Global" in interface.extended_attributes and interface.supports_named_properties: named_properties_class = f"{interface.name}Properties" out.write( diff --git a/Meta/Utils/webidl_parser.py b/Meta/Utils/webidl_parser.py index 5547beca73..4e5b23a7eb 100644 --- a/Meta/Utils/webidl_parser.py +++ b/Meta/Utils/webidl_parser.py @@ -4,24 +4,191 @@ from dataclasses import dataclass from dataclasses import field +from enum import Enum from pathlib import Path from typing import Dict from typing import List from typing import NoReturn from typing import Optional +from typing import Sequence +from typing import Tuple from Utils.lexer import Lexer +@dataclass(frozen=True) +class IDLType: + name: str + nullable: bool = False + + def __str__(self) -> str: + nullable_suffix = "?" if self.nullable else "" + return f"{self.name}{nullable_suffix}" + + def clone_with_nullable(self, nullable: bool) -> "IDLType": + return IDLType(self.name, nullable) + + def without_nullable(self) -> "IDLType": + return self.clone_with_nullable(False) + + def flattened_member_types(self) -> List["IDLType"]: + return [self] + + def includes_undefined(self) -> bool: + return self.name == "undefined" + + def includes_nullable_type(self) -> bool: + return self.nullable + + def child_types(self) -> Tuple["IDLType", ...]: + return () + + def nested_types(self) -> List["IDLType"]: + nested_types: List["IDLType"] = [self] + for child_type in self.child_types(): + nested_types.extend(child_type.nested_types()) + return nested_types + + +@dataclass(init=False, frozen=True) +class IDLUnionType(IDLType): + member_types: Tuple[IDLType, ...] + + def __init__(self, member_types: Sequence[IDLType], nullable: bool = False) -> None: + object.__setattr__(self, "member_types", tuple(member_types)) + super().__init__(self.name_for_member_types(member_types), nullable) + + @staticmethod + def name_for_member_types(member_types: Sequence[IDLType]) -> str: + return f"({' or '.join(str(member_type) for member_type in member_types)})" + + def __str__(self) -> str: + nullable_suffix = "?" if self.nullable else "" + return f"{self.name_for_member_types(self.member_types)}{nullable_suffix}" + + def clone_with_nullable(self, nullable: bool) -> "IDLUnionType": + return IDLUnionType(self.member_types, nullable) + + def flattened_member_types(self) -> List[IDLType]: + flattened_member_types: List[IDLType] = [] + for member_type in self.member_types: + flattened_member_types.extend(member_type.flattened_member_types()) + return flattened_member_types + + def includes_undefined(self) -> bool: + return any(member_type.includes_undefined() for member_type in self.member_types) + + def includes_nullable_type(self) -> bool: + return self.nullable or any(member_type.includes_nullable_type() for member_type in self.member_types) + + def child_types(self) -> Tuple[IDLType, ...]: + return self.member_types + + +@dataclass(init=False, frozen=True) +class IDLParameterizedType(IDLType): + parameters: Tuple[IDLType, ...] + + def __init__(self, name: str, parameters: Sequence[IDLType], nullable: bool = False) -> None: + object.__setattr__(self, "parameters", tuple(parameters)) + super().__init__(name, nullable) + + def __str__(self) -> str: + nullable_suffix = "?" if self.nullable else "" + return f"{self.name}<{', '.join(str(parameter) for parameter in self.parameters)}>{nullable_suffix}" + + def clone_with_nullable(self, nullable: bool) -> "IDLParameterizedType": + return IDLParameterizedType(self.name, self.parameters, nullable) + + def child_types(self) -> Tuple[IDLType, ...]: + return self.parameters + + @dataclass class Constant: - declaration: str + type: IDLType + name: str + value: str + + +@dataclass +class OperationParameter: + name: str + type: IDLType + optional: bool = False + default_value: Optional[str] = None + variadic: bool = False + extended_attributes: Dict[str, str] = field(default_factory=dict) @dataclass class SpecialOperation: - identifier_type: str - declaration: str + identifier_type: IDLType + return_type: IDLType + name: str + parameters: List[OperationParameter] + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class Operation: + name: str + return_type: IDLType + parameters: List[OperationParameter] + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class Constructor: + parameters: List[OperationParameter] + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class IterableDeclaration: + value_type: IDLType + key_type: Optional[IDLType] = None + + +@dataclass +class AsyncIterableDeclaration: + value_type: IDLType + parameters: List[OperationParameter] = field(default_factory=list) + key_type: Optional[IDLType] = None + + +@dataclass +class MaplikeDeclaration: + key_type: IDLType + value_type: IDLType + readonly: bool = False + + +@dataclass +class SetlikeDeclaration: + value_type: IDLType + readonly: bool = False + + +@dataclass +class Attribute: + name: str + type: IDLType + readonly: bool = False + inherit: bool = False + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class Stringifier: + extended_attributes: Dict[str, str] = field(default_factory=dict) + attribute: Optional[Attribute] = None + + +class InterfaceKind(Enum): + Interface = "interface" + Namespace = "namespace" + CallbackInterface = "callback interface" @dataclass @@ -29,75 +196,131 @@ class Interface: name: str path: Path extended_attributes: Dict[str, str] = field(default_factory=dict) - is_namespace: bool = False - is_callback_interface: bool = False + kind: InterfaceKind = InterfaceKind.Interface parent_name: str = "" constants: List[Constant] = field(default_factory=list) + regular_attributes: List[Attribute] = field(default_factory=list) + static_attributes: List[Attribute] = field(default_factory=list) + regular_operations: List[Operation] = field(default_factory=list) + static_operations: List[Operation] = field(default_factory=list) + constructors: List[Constructor] = field(default_factory=list) + stringifier: Optional[Stringifier] = None + iterable: Optional[IterableDeclaration] = None + async_iterable: Optional[AsyncIterableDeclaration] = None named_property_getter: Optional[SpecialOperation] = None indexed_property_getter: Optional[SpecialOperation] = None - has_special_member: bool = False - implemented_name: str = "" - namespaced_name: str = "" - constructor_class: str = "" - prototype_class: str = "" - namespace_class: str = "" + named_property_setter: Optional[SpecialOperation] = None + named_property_deleter: Optional[SpecialOperation] = None + indexed_property_setter: Optional[SpecialOperation] = None + maplike: Optional[MaplikeDeclaration] = None + setlike: Optional[SetlikeDeclaration] = None - def supports_named_properties(self) -> bool: - return self.named_property_getter is not None + @property + def is_namespace(self) -> bool: + return self.kind == InterfaceKind.Namespace - def finalize(self) -> None: + @property + def is_callback_interface(self) -> bool: + return self.kind == InterfaceKind.CallbackInterface + + @property + def has_special_member(self) -> bool: + return ( + self.iterable is not None + or self.async_iterable is not None + or self.maplike is not None + or self.setlike is not None + ) + + @property + def namespaced_name(self) -> str: legacy_namespace = self.extended_attributes.get("LegacyNamespace") if legacy_namespace: - self.namespaced_name = f"{legacy_namespace}.{self.name}" - else: - self.namespaced_name = self.name + return f"{legacy_namespace}.{self.name}" + return self.name - self.implemented_name = self.extended_attributes.get("ImplementedAs", self.name) - self.constructor_class = f"{self.implemented_name}Constructor" - self.prototype_class = f"{self.implemented_name}Prototype" - self.namespace_class = f"{self.name}Namespace" + @property + def implemented_name(self) -> str: + return self.extended_attributes.get("ImplementedAs", self.name) + + @property + def constructor_class(self) -> str: + return f"{self.implemented_name}Constructor" + + @property + def prototype_class(self) -> str: + return f"{self.implemented_name}Prototype" + + @property + def namespace_class(self) -> str: + return f"{self.name}Namespace" + + @property + def supports_named_properties(self) -> bool: + return self.named_property_getter is not None @dataclass class Dictionary: name: str path: Path + parent_name: str = "" + members: List["DictionaryMember"] = field(default_factory=list) + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class DictionaryMember: + name: str + type: IDLType + required: bool = False + default_value: Optional[str] = None + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class CallbackFunction: + name: str + path: Path + return_type: IDLType + parameters: List[OperationParameter] = field(default_factory=list) + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class Enumeration: + name: str + path: Path + values: List[str] + extended_attributes: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class Typedef: + name: str + path: Path + type: IDLType + + +@dataclass +class IncludedMixin: + interface_name: str + mixin_name: str @dataclass class Module: path: Path interface: Optional[Interface] = None + partial_interfaces: List[Interface] = field(default_factory=list) + mixins: List[Interface] = field(default_factory=list) + partial_mixins: List[Interface] = field(default_factory=list) + included_mixins: List[IncludedMixin] = field(default_factory=list) + callback_functions: List[CallbackFunction] = field(default_factory=list) dictionaries: List[Dictionary] = field(default_factory=list) - - -@dataclass -class NestingState: - parentheses_depth: int = 0 - bracket_depth: int = 0 - brace_depth: int = 0 - angle_depth: int = 0 - - def is_at_top_level(self) -> bool: - return not any((self.parentheses_depth, self.bracket_depth, self.brace_depth, self.angle_depth)) - - def update_for_character(self, character: str) -> None: - if character == "(": - self.parentheses_depth += 1 - elif character == ")": - self.parentheses_depth = max(self.parentheses_depth - 1, 0) - elif character == "[": - self.bracket_depth += 1 - elif character == "]": - self.bracket_depth = max(self.bracket_depth - 1, 0) - elif character == "{": - self.brace_depth += 1 - elif character == "}": - self.brace_depth = max(self.brace_depth - 1, 0) - elif character == "<": - self.angle_depth += 1 - elif character == ">": - self.angle_depth = max(self.angle_depth - 1, 0) + partial_dictionaries: List[Dictionary] = field(default_factory=list) + enumerations: List[Enumeration] = field(default_factory=list) + typedefs: List[Typedef] = field(default_factory=list) class ParseError(RuntimeError): @@ -119,67 +342,66 @@ class Parser: self.consume_whitespace() while not self.lexer.is_eof(): - extended_attributes: Dict[str, str] = {} - if self.lexer.consume_specific("["): - extended_attributes = self.parse_extended_attributes() - - if self.next_is_keyword("dictionary") or self.next_is_keyword("partial dictionary"): - dictionary = self.parse_dictionary() - if dictionary is not None: - module.dictionaries.append(dictionary) - elif self.next_is_keyword("enum"): - self.skip_braced_declaration() - elif self.next_is_keyword("typedef"): - self.consume_keyword("typedef") - self.consume_statement_text() - elif self.next_is_keyword("partial interface mixin"): - self.skip_braced_declaration() - elif self.next_is_keyword("partial interface"): - self.skip_braced_declaration() - elif self.next_is_keyword("interface mixin"): - self.skip_braced_declaration() - elif self.next_is_keyword("partial namespace"): - self.skip_braced_declaration() - elif self.next_is_keyword("callback interface"): - module.interface = self.set_or_check_module_interface( - module.interface, - self.parse_interface(extended_attributes, is_callback_interface=True), - ) - elif self.next_is_keyword("callback"): - self.consume_keyword("callback") - self.consume_statement_text() - elif self.next_is_keyword("namespace"): - module.interface = self.set_or_check_module_interface( - module.interface, - self.parse_interface(extended_attributes, is_namespace=True), - ) - elif self.next_is_keyword("interface"): - module.interface = self.set_or_check_module_interface( - module.interface, - self.parse_interface(extended_attributes), - ) - else: - self.parse_includes_statement() - + self.parse_module_declaration(module, self.parse_leading_extended_attributes()) self.consume_whitespace() return module - def set_or_check_module_interface( - self, existing_interface: Optional[Interface], candidate_interface: Interface - ) -> Interface: - if existing_interface is None: - return candidate_interface + def parse_module_declaration(self, module: Module, extended_attributes: Dict[str, str]) -> None: + if self.consume_optional_keyword("dictionary"): + module.dictionaries.append(self.parse_dictionary(extended_attributes)) + return - if not existing_interface.is_namespace and existing_interface.name == candidate_interface.name: - return existing_interface + if self.consume_optional_keyword("partial dictionary"): + module.partial_dictionaries.append(self.parse_dictionary(extended_attributes)) + return - self.raise_parse_error( - "encountered multiple interface, callback interface, or namespace declarations in one file" - ) + if self.consume_optional_keyword("enum"): + module.enumerations.append(self.parse_enumeration(extended_attributes)) + return - def parse_includes_statement(self) -> None: - self.parse_identifier_ending_with_space() + if self.consume_optional_keyword("typedef"): + module.typedefs.append(self.parse_typedef()) + return + + if self.consume_optional_keyword("partial interface mixin"): + module.partial_mixins.append(self.parse_interface_declaration(extended_attributes)) + return + + if self.consume_optional_keyword("partial interface"): + module.partial_interfaces.append(self.parse_interface_declaration(extended_attributes)) + return + + if self.consume_optional_keyword("interface mixin"): + module.mixins.append(self.parse_interface_declaration(extended_attributes)) + return + + if self.consume_optional_keyword("partial namespace"): + module.partial_interfaces.append( + self.parse_interface_declaration(extended_attributes, InterfaceKind.Namespace) + ) + return + + if self.consume_optional_keyword("callback interface"): + module.interface = self.parse_interface_declaration(extended_attributes, InterfaceKind.CallbackInterface) + return + + if self.consume_optional_keyword("callback"): + module.callback_functions.append(self.parse_callback_function(extended_attributes)) + return + + if self.consume_optional_keyword("namespace"): + module.interface = self.parse_interface_declaration(extended_attributes, InterfaceKind.Namespace) + return + + if self.consume_optional_keyword("interface"): + module.interface = self.parse_interface_declaration(extended_attributes) + return + + module.included_mixins.append(self.parse_includes_statement()) + + def parse_includes_statement(self) -> IncludedMixin: + interface_name = self.parse_identifier() self.consume_whitespace() if not self.next_is_keyword("includes"): @@ -187,106 +409,288 @@ class Parser: self.consume_keyword("includes") self.consume_whitespace() - self.parse_identifier_ending_with_space_or(";") + mixin_name = self.parse_identifier() self.consume_whitespace() self.assert_specific(";") + return IncludedMixin(interface_name=interface_name, mixin_name=mixin_name) - def parse_interface( - self, - extended_attributes: Dict[str, str], - is_namespace: bool = False, - is_callback_interface: bool = False, + def parse_interface_declaration( + self, extended_attributes: Dict[str, str], kind: InterfaceKind = InterfaceKind.Interface ) -> Interface: - if is_callback_interface: - self.consume_keyword("callback") - self.consume_whitespace() - self.consume_keyword("interface") - elif is_namespace: - self.consume_keyword("namespace") - else: - self.consume_keyword("interface") - - self.consume_whitespace() - + name, parent_name = self.parse_named_body_header() interface = Interface( - name=self.parse_identifier_ending_with_space_or(":", "{"), + name=name, path=self.path, extended_attributes=extended_attributes, - is_namespace=is_namespace, - is_callback_interface=is_callback_interface, + kind=kind, + parent_name=parent_name, ) - - self.consume_whitespace() - if not is_namespace and self.lexer.consume_specific(":"): - self.consume_whitespace() - interface.parent_name = self.parse_identifier_ending_with_space_or("{") - self.consume_whitespace() - - body_text = self.consume_braced_block() + self.parse_interface_body(interface) self.consume_whitespace() self.assert_specific(";") - if not is_namespace: - self.parse_interface_body(interface, body_text) - - interface.finalize() return interface - def parse_dictionary(self) -> Optional[Dictionary]: - is_partial = False - if self.next_is_keyword("partial"): - self.consume_keyword("partial") - self.consume_whitespace() - is_partial = True + def parse_dictionary(self, extended_attributes: Dict[str, str]) -> Dictionary: + dictionary_extended_attributes = extended_attributes + dictionary_name, parent_name = self.parse_named_body_header() + members: List[DictionaryMember] = [] - self.consume_keyword("dictionary") + while True: + self.consume_whitespace() + + if self.lexer.consume_specific("}"): + self.consume_whitespace() + self.assert_specific(";") + break + + member_extended_attributes = self.parse_leading_extended_attributes() + + required = self.consume_optional_keyword("required") + + member_extended_attributes.update(self.parse_leading_extended_attributes()) + + member_type = self.parse_type() + self.consume_whitespace() + member_name = self.parse_identifier() + self.consume_whitespace() + + default_value: Optional[str] = None + if self.lexer.consume_specific("="): + if required: + self.raise_parse_error("required dictionary member must not have a default value") + self.consume_whitespace() + default_value = self.consume_until_top_level(";").strip() + self.consume_whitespace() + + self.assert_specific(";") + + members.append( + DictionaryMember( + name=member_name, + type=member_type, + required=required, + default_value=default_value, + extended_attributes=member_extended_attributes, + ) + ) + + members.sort(key=lambda member: member.name) + + return Dictionary( + name=dictionary_name, + path=self.path, + parent_name=parent_name, + members=members, + extended_attributes=dictionary_extended_attributes, + ) + + def parse_callback_function(self, extended_attributes: Dict[str, str]) -> CallbackFunction: + name = self.parse_identifier() + self.consume_whitespace() + self.assert_specific("=") self.consume_whitespace() - dictionary_name = self.parse_identifier_ending_with_space_or(":", "{") + return_type = self.parse_type() + self.consume_whitespace() + parameters = self.parse_parenthesized_parameters() + self.assert_specific(";") + + return CallbackFunction( + name=name, + path=self.path, + return_type=return_type, + parameters=parameters, + extended_attributes=extended_attributes, + ) + + def parse_typedef(self) -> Typedef: + typedef_type = self.parse_type() self.consume_whitespace() - if self.lexer.consume_specific(":"): - self.consume_whitespace() - self.parse_identifier_ending_with_space_or("{") - self.consume_whitespace() - - self.consume_braced_block() + name = self.parse_identifier() self.consume_whitespace() self.assert_specific(";") - if is_partial: - return None + return Typedef(name=name, path=self.path, type=typedef_type) - return Dictionary(name=dictionary_name, path=self.path) + def parse_type(self) -> IDLType: + if self.lexer.consume_specific("("): + return self.parse_union_type() - def parse_interface_body(self, interface: Interface, body_text: str) -> None: - for statement in split_top_level_statements(remove_line_comments(body_text)): - if not statement: + type_name = self.parse_type_name() + parameters: List[IDLType] = [] + if self.lexer.next_is("<"): + parameters = self.parse_type_parameters() + + nullable = self.lexer.consume_specific("?") + if parameters: + return IDLParameterizedType(type_name, parameters, nullable) + return IDLType(type_name, nullable) + + def parse_union_type(self) -> IDLUnionType: + member_types = [self.parse_type()] + self.consume_whitespace() + self.consume_keyword("or") + self.consume_whitespace() + member_types.append(self.parse_type()) + self.consume_whitespace() + + while self.lexer.consume_specific("or"): + self.consume_whitespace() + member_types.append(self.parse_type()) + self.consume_whitespace() + + self.assert_specific(")") + return IDLUnionType(member_types, self.lexer.consume_specific("?")) + + def parse_type_name(self) -> str: + type_words: List[str] = [] + if self.consume_optional_keyword("unsigned"): + type_words.append("unsigned") + if self.consume_optional_keyword("unrestricted"): + type_words.append("unrestricted") + + name = self.lexer.consume_while(lambda character: character.isalnum() or character == "_") + if not name: + self.raise_parse_error("type can't be an empty string") + type_words.append(name) + + if name.lower() == "long": + position_before_whitespace = self.lexer.tell() + self.consume_whitespace() + if self.lexer.consume_specific("long"): + type_words.append("long") + else: + self.lexer.position = position_before_whitespace + + return " ".join(type_words) + + def parse_named_body_header(self) -> Tuple[str, str]: + name = self.parse_identifier() + self.consume_whitespace() + + parent_name = "" + if self.lexer.consume_specific(":"): + self.consume_whitespace() + parent_name = self.parse_identifier() + self.consume_whitespace() + + self.assert_specific("{") + return name, parent_name + + def parse_enumeration(self, extended_attributes: Dict[str, str]) -> Enumeration: + name = self.parse_identifier() + self.consume_whitespace() + self.assert_specific("{") + + values: List[str] = [] + + while not self.lexer.is_eof(): + self.consume_whitespace() + if self.lexer.consume_specific("}"): + break + + self.assert_specific('"') + value = self.lexer.consume_until(lambda character: character == '"') + self.assert_specific('"') + self.consume_whitespace() + + values.append(value) + + self.consume_whitespace() + if self.lexer.next_is("}"): + continue + self.assert_specific(",") + + self.consume_whitespace() + self.assert_specific(";") + + return Enumeration( + name=name, + path=self.path, + values=values, + extended_attributes=extended_attributes, + ) + + def parse_interface_body(self, interface: Interface) -> None: + while not self.lexer.is_eof(): + self.consume_whitespace() + if self.lexer.consume_specific("}"): + return + + extended_attributes = self.parse_leading_extended_attributes() + self.consume_whitespace() + + if self.next_is_keyword("const"): + interface.constants.append(self.parse_constant()) continue - stripped_statement = strip_leading_extended_attributes(statement).strip() - if not stripped_statement: + if self.consume_optional_keyword("static"): + readonly = self.consume_optional_keyword("readonly") + if readonly or self.next_is_keyword("attribute"): + interface.static_attributes.append(self.parse_attribute(extended_attributes, readonly=readonly)) + else: + interface.static_operations.append(self.parse_operation(extended_attributes)) continue - if stripped_statement.startswith("const "): - interface.constants.append(Constant(stripped_statement)) + if self.consume_optional_keyword("stringifier"): + readonly = self.consume_optional_keyword("readonly") + if readonly or self.next_is_keyword("attribute"): + attribute = self.parse_attribute(extended_attributes, readonly=readonly) + interface.regular_attributes.append(attribute) + interface.stringifier = Stringifier(extended_attributes, attribute) + continue + + self.consume_member_semicolon() + interface.stringifier = Stringifier(extended_attributes) continue - if ( - stripped_statement.startswith("iterable<") - or stripped_statement.startswith("async iterable<") - or stripped_statement.startswith("maplike<") - or stripped_statement.startswith("setlike<") - ): - interface.has_special_member = True + if self.consume_optional_keyword("inherit"): + interface.regular_attributes.append(self.parse_attribute(extended_attributes, inherit=True)) + continue - if stripped_statement.startswith("getter "): - identifier_type = parse_special_operation_identifier_type(stripped_statement) - special_operation = SpecialOperation(identifier_type=identifier_type, declaration=stripped_statement) + readonly = self.consume_optional_keyword("readonly") + if self.next_is_keyword("attribute"): + interface.regular_attributes.append( + self.parse_attribute( + extended_attributes, + readonly=readonly, + ) + ) + continue - if identifier_type == "DOMString": + if self.next_is_keyword("maplike"): + interface.maplike = self.parse_maplike_declaration(readonly=readonly) + continue + + if self.next_is_keyword("setlike"): + interface.setlike = self.parse_setlike_declaration(readonly=readonly) + continue + + if readonly: + interface.regular_attributes.append(self.parse_attribute(extended_attributes, readonly=True)) + continue + + if self.next_is_keyword("constructor"): + interface.constructors.append(self.parse_constructor(extended_attributes)) + continue + + if self.next_is_keyword("iterable"): + interface.iterable = self.parse_iterable_declaration() + continue + + if self.next_is_keyword("async"): + interface.async_iterable = self.parse_async_iterable_declaration() + continue + + if self.next_is_keyword("getter"): + special_operation = self.parse_special_operation("getter", extended_attributes) + identifier_type = special_operation.identifier_type + + if identifier_type.name == "DOMString" and not identifier_type.nullable: interface.named_property_getter = special_operation - elif identifier_type == "unsigned long": + elif identifier_type.name == "unsigned long" and not identifier_type.nullable: interface.indexed_property_getter = special_operation else: self.raise_parse_error( @@ -294,6 +698,262 @@ class Parser: ) continue + if self.next_is_keyword("setter"): + special_operation = self.parse_special_operation("setter", extended_attributes) + identifier_type = special_operation.identifier_type + + if identifier_type.name == "DOMString" and not identifier_type.nullable: + interface.named_property_setter = special_operation + elif identifier_type.name == "unsigned long" and not identifier_type.nullable: + interface.indexed_property_setter = special_operation + else: + self.raise_parse_error( + f"named/indexed property setter must use DOMString or unsigned long, got '{identifier_type}'" + ) + continue + + if self.next_is_keyword("deleter"): + special_operation = self.parse_special_operation("deleter", extended_attributes) + identifier_type = special_operation.identifier_type + + if identifier_type.name == "DOMString" and not identifier_type.nullable: + interface.named_property_deleter = special_operation + else: + self.raise_parse_error(f"named property deleter must use DOMString, got '{identifier_type}'") + continue + + interface.regular_operations.append(self.parse_operation(extended_attributes)) + + self.raise_parse_error("unterminated interface body") + + def parse_iterable_declaration(self) -> IterableDeclaration: + self.consume_keyword("iterable") + key_type, value_type = self.parse_iterable_type_parameters() + self.consume_member_semicolon() + + return IterableDeclaration( + value_type=value_type, + key_type=key_type, + ) + + def parse_async_iterable_declaration(self) -> AsyncIterableDeclaration: + self.consume_keyword("async") + self.consume_whitespace() + self.consume_keyword("iterable") + key_type, value_type = self.parse_iterable_type_parameters() + self.consume_whitespace() + + parameters: List[OperationParameter] = [] + if self.lexer.next_is("("): + parameters = self.parse_parenthesized_parameters() + + self.consume_member_semicolon() + + return AsyncIterableDeclaration( + value_type=value_type, + key_type=key_type, + parameters=parameters, + ) + + def parse_iterable_type_parameters(self) -> Tuple[Optional[IDLType], IDLType]: + parameters = self.parse_type_parameters() + if len(parameters) == 1: + return None, parameters[0] + if len(parameters) == 2: + return parameters[0], parameters[1] + self.raise_parse_error("expected one or two iterable type parameters") + + def parse_type_parameters(self) -> List[IDLType]: + self.assert_specific("<") + parameters = [self.parse_type()] + self.consume_whitespace() + + while self.lexer.consume_specific(","): + self.consume_whitespace() + parameters.append(self.parse_type()) + self.consume_whitespace() + + self.assert_specific(">") + return parameters + + def parse_maplike_declaration(self, readonly: bool = False) -> MaplikeDeclaration: + self.consume_keyword("maplike") + parameters = self.parse_type_parameters() + if len(parameters) != 2: + self.raise_parse_error("expected two maplike type parameters") + self.consume_member_semicolon() + + return MaplikeDeclaration( + key_type=parameters[0], + value_type=parameters[1], + readonly=readonly, + ) + + def parse_setlike_declaration(self, readonly: bool = False) -> SetlikeDeclaration: + self.consume_keyword("setlike") + parameters = self.parse_type_parameters() + if len(parameters) != 1: + self.raise_parse_error("expected one setlike type parameter") + self.consume_member_semicolon() + + return SetlikeDeclaration( + value_type=parameters[0], + readonly=readonly, + ) + + def parse_special_operation( + self, + keyword: str, + extended_attributes: Dict[str, str], + ) -> SpecialOperation: + self.consume_keyword(keyword) + self.consume_whitespace() + + return_type = self.parse_type() + self.consume_whitespace() + + name = "" + if not self.lexer.next_is("("): + name = self.parse_identifier() + self.consume_whitespace() + + parameters = self.parse_parenthesized_parameters() + self.consume_member_semicolon() + + if not parameters: + self.raise_parse_error("special operation must have an identifier parameter") + + return SpecialOperation( + identifier_type=parameters[0].type, + return_type=return_type, + name=name, + parameters=parameters, + extended_attributes=extended_attributes, + ) + + def parse_parenthesized_parameters(self) -> List[OperationParameter]: + self.assert_specific("(") + self.consume_whitespace() + + parameters: List[OperationParameter] = [] + if not self.lexer.next_is(")"): + parameters = self.parse_operation_parameters() + self.consume_whitespace() + + self.assert_specific(")") + self.consume_whitespace() + return parameters + + def parse_operation_parameters(self) -> List[OperationParameter]: + parameters: List[OperationParameter] = [] + + self.consume_whitespace() + while not self.lexer.next_is(")"): + extended_attributes = self.parse_leading_extended_attributes() + + optional = self.consume_optional_keyword("optional") + + extended_attributes.update(self.parse_leading_extended_attributes()) + + parameter_type = self.parse_type() + self.consume_whitespace() + + variadic = self.lexer.consume_specific("...") + self.consume_whitespace() + + parameter_name = self.parse_identifier() + + self.consume_whitespace() + default_value: Optional[str] = None + if self.lexer.consume_specific("="): + default_value = self.consume_until_top_level(",", ")").strip() + self.consume_whitespace() + parameters.append( + OperationParameter( + name=parameter_name, + type=parameter_type, + optional=optional, + default_value=default_value, + variadic=variadic, + extended_attributes=extended_attributes, + ) + ) + if not self.lexer.consume_specific(","): + break + self.consume_whitespace() + + return parameters + + def parse_operation(self, extended_attributes: Dict[str, str]) -> Operation: + return_type = self.parse_type() + self.consume_whitespace() + name = self.parse_identifier() + self.consume_whitespace() + parameters = self.parse_parenthesized_parameters() + self.consume_member_semicolon() + + return Operation( + name=name, + return_type=return_type, + parameters=parameters, + extended_attributes=extended_attributes, + ) + + def parse_constructor(self, extended_attributes: Dict[str, str]) -> Constructor: + self.consume_keyword("constructor") + self.consume_whitespace() + parameters = self.parse_parenthesized_parameters() + self.consume_member_semicolon() + + return Constructor( + parameters=parameters, + extended_attributes=extended_attributes, + ) + + def parse_constant(self) -> Constant: + self.consume_keyword("const") + self.consume_whitespace() + + constant_type = self.parse_type() + self.consume_whitespace() + + name = self.parse_identifier() + self.consume_whitespace() + self.assert_specific("=") + self.consume_whitespace() + + value = self.consume_until_top_level(";").strip() + self.consume_member_semicolon() + + return Constant( + type=constant_type, + name=name, + value=value, + ) + + def parse_attribute( + self, + extended_attributes: Dict[str, str], + readonly: bool = False, + inherit: bool = False, + ) -> Attribute: + self.consume_keyword("attribute") + self.consume_whitespace() + + attribute_type = self.parse_type() + self.consume_whitespace() + + name = self.parse_identifier() + self.consume_member_semicolon() + + return Attribute( + name=name, + type=attribute_type, + readonly=readonly, + inherit=inherit, + extended_attributes=extended_attributes, + ) + def parse_extended_attributes(self) -> Dict[str, str]: extended_attributes: Dict[str, str] = {} @@ -302,10 +962,10 @@ class Parser: if self.lexer.consume_specific("]"): break - name = self.parse_identifier_ending_with_space_or("]", "=", ",") + name = self.parse_identifier() value = "" if self.lexer.consume_specific("="): - value = self.consume_extended_attribute_value().strip() + value = self.consume_until_top_level(",", "]").strip() extended_attributes[name] = value self.lexer.consume_specific(",") @@ -313,105 +973,56 @@ class Parser: self.consume_whitespace() return extended_attributes - def consume_extended_attribute_value(self) -> str: - start = self.lexer.tell() - parentheses_depth = 0 + def parse_leading_extended_attributes(self) -> Dict[str, str]: + self.consume_whitespace() + if self.lexer.consume_specific("["): + return self.parse_extended_attributes() + return {} - while not self.lexer.is_eof(): - character = self.lexer.peek() - if character == "(": - parentheses_depth += 1 - elif character == ")": - if parentheses_depth > 0: - parentheses_depth -= 1 - elif parentheses_depth == 0 and character in (",", "]"): - break - - self.lexer.consume() - - return self.contents[start : self.lexer.tell()] - - def skip_braced_declaration(self) -> None: - while not self.lexer.is_eof() and self.lexer.peek() != "{": - self.lexer.consume() - - if self.lexer.is_eof(): - self.raise_parse_error("expected '{' while skipping declaration") - - self.consume_braced_block() + def consume_member_semicolon(self) -> None: self.consume_whitespace() self.assert_specific(";") - def consume_braced_block(self) -> str: - self.assert_specific("{") - + def consume_until_top_level(self, *terminators: str) -> str: start = self.lexer.tell() - brace_depth = 1 + delimiter_stack: List[str] = [] active_quote = "" + matching_delimiter = { + "(": ")", + "[": "]", + "{": "}", + "<": ">", + } while not self.lexer.is_eof(): - character = self.lexer.consume() + character = self.lexer.peek() if active_quote: + self.lexer.consume() if character == active_quote: active_quote = "" continue - if character == "/" and self.lexer.peek() == "/": - self.lexer.ignore_until("\n") - if self.lexer.peek() == "\n": - self.lexer.ignore() - continue - if character in ('"', "'"): active_quote = character + self.lexer.consume() continue - if character == "{": - brace_depth += 1 - elif character == "}": - brace_depth -= 1 - if brace_depth == 0: - end = self.lexer.tell() - 1 - return self.contents[start:end] + if character in terminators and not delimiter_stack: + return self.contents[start : self.lexer.tell()] - self.raise_parse_error("unterminated declaration body") - - def consume_statement_text(self) -> str: - start = self.lexer.tell() - nesting_state = NestingState() - active_quote = "" - - while not self.lexer.is_eof(): character = self.lexer.consume() + if character in matching_delimiter: + delimiter_stack.append(matching_delimiter[character]) + elif delimiter_stack and character == delimiter_stack[-1]: + delimiter_stack.pop() - if active_quote: - if character == active_quote: - active_quote = "" - continue + return self.contents[start : self.lexer.tell()] - if character == "/" and self.lexer.peek() == "/": - self.lexer.ignore_until("\n") - if self.lexer.peek() == "\n": - self.lexer.ignore() - continue - - if character in ('"', "'"): - active_quote = character - continue - - if character == ";" and nesting_state.is_at_top_level(): - end = self.lexer.tell() - 1 - return self.contents[start:end] - nesting_state.update_for_character(character) - - self.raise_parse_error("unterminated statement") - - def parse_identifier_ending_with_space(self) -> str: - return self.parse_identifier_ending_with_space_or() - - def parse_identifier_ending_with_space_or(self, *terminators: str) -> str: - identifier = self.lexer.consume_until(lambda character: character.isspace() or character in terminators) + def parse_identifier(self) -> str: + identifier = self.lexer.consume_while(lambda character: character.isalnum() or character in "_-") + if not identifier: + self.raise_parse_error("expected identifier") return identifier.lstrip("_") def consume_whitespace(self) -> None: @@ -438,7 +1049,15 @@ class Parser: return True trailing_character = self.contents[end] - return trailing_character.isspace() or trailing_character in "{([;:" + return trailing_character.isspace() or trailing_character in "{([;:<" + + def consume_optional_keyword(self, keyword: str) -> bool: + if not self.next_is_keyword(keyword): + return False + + self.consume_keyword(keyword) + self.consume_whitespace() + return True def consume_keyword(self, keyword: str) -> None: if not self.next_is_keyword(keyword): @@ -463,144 +1082,3 @@ class Parser: column_number += 1 raise ParseError(f"{self.path}:{line_number}:{column_number}: error: {message}") - - -def remove_line_comments(text: str) -> str: - lines = [] - for line in text.splitlines(): - comment_index = line.find("//") - if comment_index != -1: - line = line[:comment_index] - lines.append(line) - return "\n".join(lines) - - -def split_top_level_statements(text: str) -> List[str]: - statements: List[str] = [] - start = 0 - nesting_state = NestingState() - active_quote = "" - - for index, character in enumerate(text): - if active_quote: - if character == active_quote: - active_quote = "" - continue - - if character in ('"', "'"): - active_quote = character - continue - - if character == ";" and nesting_state.is_at_top_level(): - statements.append(text[start:index].strip()) - start = index + 1 - continue - - nesting_state.update_for_character(character) - - trailing_text = text[start:].strip() - if trailing_text: - statements.append(trailing_text) - - return statements - - -def strip_leading_extended_attributes(text: str) -> str: - remaining_text = text.lstrip() - while remaining_text.startswith("["): - closing_bracket = find_matching_closer(remaining_text, 0, "[", "]") - remaining_text = remaining_text[closing_bracket + 1 :].lstrip() - return remaining_text - - -def parse_special_operation_identifier_type(statement: str) -> str: - parenthesized_text = extract_final_parenthesized_group(statement) - parameter_text = split_top_level_items(parenthesized_text, ",")[0].strip() - parameter_text = strip_leading_extended_attributes(parameter_text) - - last_space = find_last_top_level_whitespace(parameter_text) - if last_space == -1: - raise ParseError(f"could not determine getter identifier type from '{statement}'") - - return normalize_whitespace(parameter_text[:last_space]) - - -def extract_final_parenthesized_group(text: str) -> str: - stripped_text = text.rstrip() - if not stripped_text.endswith(")"): - raise ParseError(f"expected a trailing parameter list in '{text}'") - - closing_index = len(stripped_text) - 1 - opening_index = find_matching_opener(stripped_text, closing_index, "(", ")") - return stripped_text[opening_index + 1 : closing_index] - - -def split_top_level_items(text: str, delimiter: str) -> List[str]: - items: List[str] = [] - start = 0 - nesting_state = NestingState() - active_quote = "" - - for index, character in enumerate(text): - if active_quote: - if character == active_quote: - active_quote = "" - continue - - if character in ('"', "'"): - active_quote = character - continue - - if character == delimiter and nesting_state.is_at_top_level(): - items.append(text[start:index]) - start = index + 1 - continue - - nesting_state.update_for_character(character) - - items.append(text[start:]) - return items - - -def find_last_top_level_whitespace(text: str) -> int: - last_space = -1 - nesting_state = NestingState() - - for index, character in enumerate(text): - if character.isspace() and nesting_state.is_at_top_level(): - last_space = index - nesting_state.update_for_character(character) - - return last_space - - -def find_matching_closer(text: str, start_index: int, opener: str, closer: str) -> int: - depth = 0 - for index in range(start_index, len(text)): - character = text[index] - if character == opener: - depth += 1 - elif character == closer: - depth -= 1 - if depth == 0: - return index - - raise ParseError(f"unterminated '{opener}' in '{text}'") - - -def find_matching_opener(text: str, start_index: int, opener: str, closer: str) -> int: - depth = 0 - for index in range(start_index, -1, -1): - character = text[index] - if character == closer: - depth += 1 - elif character == opener: - depth -= 1 - if depth == 0: - return index - - raise ParseError(f"unterminated '{opener}' in '{text}'") - - -def normalize_whitespace(text: str) -> str: - return " ".join(text.split())