| Index: client/dom/scripts/databasebuilder.py
|
| ===================================================================
|
| --- client/dom/scripts/databasebuilder.py (revision 5796)
|
| +++ client/dom/scripts/databasebuilder.py (working copy)
|
| @@ -1,615 +0,0 @@
|
| -#!/usr/bin/python
|
| -# Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
| -# for details. All rights reserved. Use of this source code is governed by a
|
| -# BSD-style license that can be found in the LICENSE file.
|
| -
|
| -import copy
|
| -import database
|
| -import idlparser
|
| -import logging
|
| -import os
|
| -import os.path
|
| -
|
| -from idlnode import *
|
| -
|
| -_logger = logging.getLogger('databasebuilder')
|
| -
|
| -# Used in source annotations to specify the parent interface declaring
|
| -# a displaced declaration. The 'via' attribute specifies the parent interface
|
| -# which implements a displaced declaration.
|
| -_VIA_ANNOTATION_ATTR_NAME = 'via'
|
| -
|
| -# Used in source annotations to specify the module that the interface was
|
| -# imported from.
|
| -_MODULE_ANNOTATION_ATTR_NAME = 'module'
|
| -
|
| -
|
| -class DatabaseBuilderOptions(object):
|
| - """Used in specifying options when importing new interfaces"""
|
| -
|
| - def __init__(self,
|
| - idl_syntax=idlparser.WEBIDL_SYNTAX,
|
| - idl_defines=[],
|
| - source=None, source_attributes={},
|
| - type_rename_map={},
|
| - rename_operation_arguments_on_merge=False,
|
| - add_new_interfaces=True,
|
| - obsolete_old_declarations=False):
|
| - """Constructor.
|
| - Args:
|
| - idl_syntax -- the syntax of the IDL file that is imported.
|
| - idl_defines -- list of definitions for the idl gcc pre-processor
|
| - source -- the origin of the IDL file, used for annotating the
|
| - database.
|
| - source_attributes -- this map of attributes is used as
|
| - annotation attributes.
|
| - rename_operation_arguments_on_merge -- if True, will rename
|
| - operation arguments when merging using the new name rather
|
| - than the old.
|
| - add_new_interfaces -- when False, if an interface is a new
|
| - addition, it will be ignored.
|
| - obsolete_old_declarations -- when True, if a declaration
|
| - from a certain source is not re-declared, it will be removed.
|
| - """
|
| - self.source = source
|
| - self.source_attributes = source_attributes
|
| - self.idl_syntax = idl_syntax
|
| - self.idl_defines = idl_defines
|
| - self.type_rename_map = type_rename_map
|
| - self.rename_operation_arguments_on_merge = \
|
| - rename_operation_arguments_on_merge
|
| - self.add_new_interfaces = add_new_interfaces
|
| - self.obsolete_old_declarations = obsolete_old_declarations
|
| -
|
| -
|
| -class DatabaseBuilder(object):
|
| - def __init__(self, database):
|
| - """DatabaseBuilder is used for importing and merging interfaces into
|
| - the Database"""
|
| - self._database = database
|
| - self._imported_interfaces = []
|
| - self._impl_stmts = []
|
| - self._same_signatures = {}
|
| -
|
| - def _load_idl_file(self, file_name, import_options):
|
| - """Loads an IDL file intor memory"""
|
| - idl_parser = idlparser.IDLParser(import_options.idl_syntax)
|
| -
|
| - try:
|
| - f = open(file_name, 'r')
|
| - content = f.read()
|
| - f.close()
|
| -
|
| - idl_ast = idl_parser.parse(content,
|
| - defines=import_options.idl_defines)
|
| - return IDLFile(idl_ast, file_name)
|
| - except SyntaxError, e:
|
| - raise RuntimeError('Failed to load file %s: %s' % (file_name, e))
|
| -
|
| - def _resolve_type_defs(self, idl_file):
|
| - type_def_map = {}
|
| - # build map
|
| - for type_def in idl_file.all(IDLTypeDef):
|
| - if type_def.type.id != type_def.id: # sanity check
|
| - type_def_map[type_def.id] = type_def.type.id
|
| - # use the map
|
| - for type_node in idl_file.all(IDLType):
|
| - while type_node.id in type_def_map:
|
| - type_node.id = type_def_map[type_node.id]
|
| -
|
| - def _strip_ext_attributes(self, idl_file):
|
| - """Strips unuseful extended attributes."""
|
| - for ext_attrs in idl_file.all(IDLExtAttrs):
|
| - # TODO: Decide which attributes are uninteresting.
|
| - pass
|
| -
|
| - def _split_declarations(self, interface, optional_argument_whitelist):
|
| - """Splits read-write attributes and operations with optional
|
| - arguments into multiple declarations"""
|
| -
|
| - # split attributes into setters and getters
|
| - new_attributes = []
|
| - for attribute in interface.attributes:
|
| - if attribute.is_fc_getter or attribute.is_fc_setter:
|
| - new_attributes.append(attribute)
|
| - continue
|
| - getter_attr = copy.deepcopy(attribute)
|
| - getter_attr.is_fc_getter = True
|
| - new_attributes.append(getter_attr)
|
| - if not attribute.is_read_only:
|
| - setter_attr = copy.deepcopy(attribute)
|
| - setter_attr.is_fc_setter = True
|
| - new_attributes.append(setter_attr)
|
| - interface.attributes = new_attributes
|
| -
|
| - # Remove optional annotations from legacy optional arguments.
|
| - for op in interface.operations:
|
| - for i in range(0, len(op.arguments)):
|
| - argument = op.arguments[i]
|
| -
|
| - in_optional_whitelist = (interface.id, op.id, argument.id) in optional_argument_whitelist
|
| - if in_optional_whitelist or set(['Optional', 'Callback']).issubset(argument.ext_attrs.keys()):
|
| - argument.is_optional = True
|
| - argument.ext_attrs['RequiredCppParameter'] = None
|
| - continue
|
| -
|
| - if argument.is_optional:
|
| - if 'Optional' in argument.ext_attrs:
|
| - optional_value = argument.ext_attrs['Optional']
|
| - if optional_value:
|
| - argument.is_optional = False
|
| - del argument.ext_attrs['Optional']
|
| -
|
| - # split operations with optional args into multiple operations
|
| - new_ops = []
|
| - for op in interface.operations:
|
| - for i in range(0, len(op.arguments)):
|
| - if op.arguments[i].is_optional:
|
| - op.arguments[i].is_optional = False
|
| - new_op = copy.deepcopy(op)
|
| - new_op.arguments = new_op.arguments[:i]
|
| - new_ops.append(new_op)
|
| - new_ops.append(op)
|
| - interface.operations = new_ops
|
| -
|
| - def _rename_types(self, idl_file, import_options):
|
| - """Rename interface and type names with names provided in the
|
| - options. Also clears scopes from scoped names"""
|
| -
|
| - def rename(name):
|
| - name_parts = name.split('::')
|
| - name = name_parts[-1]
|
| - if name in import_options.type_rename_map:
|
| - name = import_options.type_rename_map[name]
|
| - return name
|
| -
|
| - def rename_node(idl_node):
|
| - idl_node.id = rename(idl_node.id)
|
| -
|
| - def rename_ext_attrs(ext_attrs_node):
|
| - for type_valued_attribute_name in ['Supplemental']:
|
| - if type_valued_attribute_name in ext_attrs_node:
|
| - value = ext_attrs_node[type_valued_attribute_name]
|
| - if isinstance(value, str):
|
| - ext_attrs_node[type_valued_attribute_name] = rename(value)
|
| -
|
| - map(rename_node, idl_file.all(IDLInterface))
|
| - map(rename_node, idl_file.all(IDLType))
|
| - map(rename_ext_attrs, idl_file.all(IDLExtAttrs))
|
| -
|
| - def _annotate(self, interface, module_name, import_options):
|
| - """Adds @ annotations based on the source and source_attributes
|
| - members of import_options."""
|
| -
|
| - source = import_options.source
|
| - if not source:
|
| - return
|
| -
|
| - def add_source_annotation(idl_node):
|
| - annotation = IDLAnnotation(
|
| - copy.deepcopy(import_options.source_attributes))
|
| - idl_node.annotations[source] = annotation
|
| - if ((isinstance(idl_node, IDLInterface) or
|
| - isinstance(idl_node, IDLMember)) and
|
| - idl_node.is_fc_suppressed):
|
| - annotation['suppressed'] = None
|
| -
|
| - add_source_annotation(interface)
|
| - interface.annotations[source][_MODULE_ANNOTATION_ATTR_NAME] = module_name
|
| -
|
| - map(add_source_annotation, interface.parents)
|
| - map(add_source_annotation, interface.constants)
|
| - map(add_source_annotation, interface.attributes)
|
| - map(add_source_annotation, interface.operations)
|
| -
|
| - def _sign(self, node):
|
| - """Computes a unique signature for the node, for merging purposed, by
|
| - concatenating types and names in the declaration."""
|
| - if isinstance(node, IDLType):
|
| - res = node.id
|
| - if res.startswith('unsigned '):
|
| - res = res[len('unsigned '):]
|
| - if res in self._same_signatures:
|
| - res = self._same_signatures[res]
|
| - return res
|
| -
|
| - res = []
|
| - if isinstance(node, IDLInterface):
|
| - res = ['interface', node.id]
|
| - elif isinstance(node, IDLParentInterface):
|
| - res = ['parent', self._sign(node.type)]
|
| - elif isinstance(node, IDLOperation):
|
| - res = ['op']
|
| - for special in node.specials:
|
| - res.append(special)
|
| - if node.id is not None:
|
| - res.append(node.id)
|
| - for arg in node.arguments:
|
| - res.append(self._sign(arg.type))
|
| - res.append(self._sign(node.type))
|
| - elif isinstance(node, IDLAttribute):
|
| - res = []
|
| - if node.is_fc_getter:
|
| - res.append('getter')
|
| - elif node.is_fc_setter:
|
| - res.append('setter')
|
| - res.append(node.id)
|
| - res.append(self._sign(node.type))
|
| - elif isinstance(node, IDLConstant):
|
| - res = []
|
| - res.append('const')
|
| - res.append(node.id)
|
| - res.append(node.value)
|
| - res.append(self._sign(node.type))
|
| - else:
|
| - raise TypeError("Can't sign input of type %s" % type(node))
|
| - return ':'.join(res)
|
| -
|
| - def _build_signatures_map(self, idl_node_list):
|
| - """Creates a hash table mapping signatures to idl_nodes for the
|
| - given list of nodes"""
|
| - res = {}
|
| - for idl_node in idl_node_list:
|
| - sig = self._sign(idl_node)
|
| - if sig is None:
|
| - continue
|
| - if sig in res:
|
| - raise RuntimeError('Warning: Multiple members have the same '
|
| - 'signature: "%s"' % sig)
|
| - res[sig] = idl_node
|
| - return res
|
| -
|
| - def _get_parent_interfaces(self, interface):
|
| - """Return a list of all the parent interfaces of a given interface"""
|
| - res = []
|
| -
|
| - def recurse(current_interface):
|
| - if current_interface in res:
|
| - return
|
| - res.append(current_interface)
|
| - for parent in current_interface.parents:
|
| - parent_name = parent.type.id
|
| - if self._database.HasInterface(parent_name):
|
| - recurse(self._database.GetInterface(parent_name))
|
| -
|
| - recurse(interface)
|
| - return res[1:]
|
| -
|
| - def _merge_ext_attrs(self, old_attrs, new_attrs):
|
| - """Merges two sets of extended attributes.
|
| -
|
| - Returns: True if old_attrs has changed.
|
| - """
|
| - changed = False
|
| - for (name, value) in new_attrs.items():
|
| - if name in old_attrs and old_attrs[name] == value:
|
| - pass # Identical
|
| - else:
|
| - old_attrs[name] = value
|
| - changed = True
|
| - return changed
|
| -
|
| - def _merge_nodes(self, old_list, new_list, import_options):
|
| - """Merges two lists of nodes. Annotates nodes with the source of each
|
| - node.
|
| -
|
| - Returns:
|
| - True if the old_list has changed.
|
| -
|
| - Args:
|
| - old_list -- the list to merge into.
|
| - new_list -- list containing more nodes.
|
| - import_options -- controls how merging is done.
|
| - """
|
| - changed = False
|
| -
|
| - source = import_options.source
|
| -
|
| - old_signatures_map = self._build_signatures_map(old_list)
|
| - new_signatures_map = self._build_signatures_map(new_list)
|
| -
|
| - # Merge new items
|
| - for (sig, new_node) in new_signatures_map.items():
|
| - if sig not in old_signatures_map:
|
| - # New node:
|
| - old_list.append(new_node)
|
| - changed = True
|
| - else:
|
| - # Merge old and new nodes:
|
| - old_node = old_signatures_map[sig]
|
| - if (source not in old_node.annotations
|
| - and source in new_node.annotations):
|
| - old_node.annotations[source] = new_node.annotations[source]
|
| - changed = True
|
| - # Maybe rename arguments:
|
| - if isinstance(old_node, IDLOperation):
|
| - for i in range(0, len(old_node.arguments)):
|
| - old_arg_name = old_node.arguments[i].id
|
| - new_arg_name = new_node.arguments[i].id
|
| - if (old_arg_name != new_arg_name
|
| - and (old_arg_name == 'arg'
|
| - or old_arg_name.endswith('Arg')
|
| - or import_options.rename_operation_arguments_on_merge)):
|
| - old_node.arguments[i].id = new_arg_name
|
| - changed = True
|
| - # Maybe merge annotations:
|
| - if (isinstance(old_node, IDLAttribute) or
|
| - isinstance(old_node, IDLOperation)):
|
| - if self._merge_ext_attrs(old_node.ext_attrs, new_node.ext_attrs):
|
| - changed = True
|
| -
|
| - # Remove annotations on obsolete items from the same source
|
| - if import_options.obsolete_old_declarations:
|
| - for (sig, old_node) in old_signatures_map.items():
|
| - if (source in old_node.annotations
|
| - and sig not in new_signatures_map):
|
| - _logger.warn('%s not available in %s anymore' %
|
| - (sig, source))
|
| - del old_node.annotations[source]
|
| - changed = True
|
| -
|
| - return changed
|
| -
|
| - def _merge_interfaces(self, old_interface, new_interface, import_options):
|
| - """Merges the new_interface into the old_interface, annotating the
|
| - interface with the sources of each change."""
|
| -
|
| - changed = False
|
| -
|
| - source = import_options.source
|
| - if (source and source not in old_interface.annotations and
|
| - source in new_interface.annotations and
|
| - not new_interface.is_supplemental):
|
| - old_interface.annotations[source] = new_interface.annotations[source]
|
| - changed = True
|
| -
|
| - def merge_list(what):
|
| - old_list = old_interface.__dict__[what]
|
| - new_list = new_interface.__dict__[what]
|
| -
|
| - if what != 'parents' and old_interface.id != new_interface.id:
|
| - for node in new_list:
|
| - node.ext_attrs['ImplementedBy'] = new_interface.id
|
| -
|
| - changed = self._merge_nodes(old_list, new_list, import_options)
|
| -
|
| - # Delete list items with zero remaining annotations.
|
| - if changed and import_options.obsolete_old_declarations:
|
| -
|
| - def has_annotations(idl_node):
|
| - return len(idl_node.annotations)
|
| -
|
| - old_interface.__dict__[what] = filter(has_annotations, old_list)
|
| -
|
| - return changed
|
| -
|
| - # Smartly merge various declarations:
|
| - if merge_list('parents'):
|
| - changed = True
|
| - if merge_list('constants'):
|
| - changed = True
|
| - if merge_list('attributes'):
|
| - changed = True
|
| - if merge_list('operations'):
|
| - changed = True
|
| -
|
| - if self._merge_ext_attrs(old_interface.ext_attrs, new_interface.ext_attrs):
|
| - changed = True
|
| -
|
| - _logger.info('merged interface %s (changed=%s, supplemental=%s)' %
|
| - (old_interface.id, changed, new_interface.is_supplemental))
|
| -
|
| - return changed
|
| -
|
| - def _merge_impl_stmt(self, impl_stmt, import_options):
|
| - """Applies "X implements Y" statemetns on the proper places in the
|
| - database"""
|
| - implementor_name = impl_stmt.implementor.id
|
| - implemented_name = impl_stmt.implemented.id
|
| - _logger.info('merging impl stmt %s implements %s' %
|
| - (implementor_name, implemented_name))
|
| -
|
| - if implementor_name == implemented_name:
|
| - # After renaming, this might happen (e.g. Window impls
|
| - # AbstractView, but AbstractView was renamed to Window).
|
| - return
|
| -
|
| - source = import_options.source
|
| - if self._database.HasInterface(implementor_name):
|
| - interface = self._database.GetInterface(implementor_name)
|
| - if interface.parents is None:
|
| - interface.parents = []
|
| - for parent in interface.parents:
|
| - if parent.type.id == implemented_name:
|
| - if source and source not in parent.annotations:
|
| - parent.annotations[source] = IDLAnnotation(
|
| - import_options.source_attributes)
|
| - return
|
| - # not found, so add new one
|
| - parent = IDLParentInterface(None)
|
| - parent.type = IDLType(implemented_name)
|
| - if source:
|
| - parent.annotations[source] = IDLAnnotation(
|
| - import_options.source_attributes)
|
| - interface.parents.append(parent)
|
| -
|
| - def set_same_signatures(self, signatures_table):
|
| - """Customize signature calculations by providing a custom table
|
| - indicating which types are equivalent"""
|
| - self._same_signatures = signatures_table
|
| -
|
| - def merge_imported_interfaces(self, optional_argument_whitelist):
|
| - """Merges all imported interfaces and loads them into the DB."""
|
| -
|
| - # Step 1: Pre process imported interfaces
|
| - for interface, module_name, import_options in self._imported_interfaces:
|
| - self._split_declarations(interface, optional_argument_whitelist)
|
| - self._annotate(interface, module_name, import_options)
|
| -
|
| - # Step 2: Add all new interfaces and merge overlapping ones
|
| - for interface, module_name, import_options in self._imported_interfaces:
|
| - if not interface.is_supplemental:
|
| - if self._database.HasInterface(interface.id):
|
| - old_interface = self._database.GetInterface(interface.id)
|
| - self._merge_interfaces(old_interface, interface, import_options)
|
| - else:
|
| - if import_options.add_new_interfaces:
|
| - self._database.AddInterface(interface)
|
| -
|
| - # Step 3: Merge in supplemental interfaces
|
| - for interface, module_name, import_options in self._imported_interfaces:
|
| - if interface.is_supplemental:
|
| - target_name = interface.ext_attrs['Supplemental']
|
| - if target_name:
|
| - # [Supplemental=DOMWindow] - merge into DOMWindow.
|
| - target = target_name
|
| - else:
|
| - # [Supplemental] - merge into existing inteface with same name.
|
| - target = interface.id
|
| - if self._database.HasInterface(target):
|
| - old_interface = self._database.GetInterface(target)
|
| - self._merge_interfaces(old_interface, interface, import_options)
|
| - else:
|
| - raise Exception("Supplemental target '%s' not found", target)
|
| -
|
| - # Step 4: Resolve 'implements' statements
|
| - for impl_stmt, import_options in self._impl_stmts:
|
| - self._merge_impl_stmt(impl_stmt, import_options)
|
| -
|
| - self._impl_stmts = []
|
| - self._imported_interfaces = []
|
| -
|
| - def import_idl_file(self, file_path,
|
| - import_options=DatabaseBuilderOptions()):
|
| - """Parses, loads into memory and cleans up and IDL file"""
|
| - idl_file = self._load_idl_file(file_path, import_options)
|
| -
|
| - self._strip_ext_attributes(idl_file)
|
| - self._resolve_type_defs(idl_file)
|
| - self._rename_types(idl_file, import_options)
|
| -
|
| - def enabled(idl_node):
|
| - return self._is_node_enabled(idl_node, import_options.idl_defines)
|
| -
|
| - for module in idl_file.modules:
|
| - for interface in module.interfaces:
|
| - if not self._is_node_enabled(interface, import_options.idl_defines):
|
| - _logger.info('skipping interface %s/%s (source=%s file=%s)'
|
| - % (module.id, interface.id, import_options.source,
|
| - file_path))
|
| - continue
|
| -
|
| - _logger.info('importing interface %s/%s (source=%s file=%s)'
|
| - % (module.id, interface.id, import_options.source,
|
| - file_path))
|
| - interface.attributes = filter(enabled, interface.attributes)
|
| - interface.operations = filter(enabled, interface.operations)
|
| - self._imported_interfaces.append((interface, module.id, import_options))
|
| -
|
| - for implStmt in module.implementsStatements:
|
| - self._impl_stmts.append((implStmt, import_options))
|
| -
|
| - def _is_node_enabled(self, node, idl_defines):
|
| - if not 'Conditional' in node.ext_attrs:
|
| - return True
|
| -
|
| - def enabled(condition):
|
| - return 'ENABLE_%s' % condition in idl_defines
|
| -
|
| - conditional = node.ext_attrs['Conditional']
|
| - if conditional.find('&') != -1:
|
| - for condition in conditional.split('&'):
|
| - if not enabled(condition):
|
| - return False
|
| - return True
|
| -
|
| - for condition in conditional.split('|'):
|
| - if enabled(condition):
|
| - return True
|
| - return False
|
| -
|
| - def import_idl_directory(self, directory_path,
|
| - import_options=DatabaseBuilderOptions()):
|
| - """Parses, loads into memory and cleans up all IDL files in a given
|
| - directory"""
|
| - if not os.path.exists(directory_path):
|
| - raise RuntimeError('directory not found: %s' % directory_path)
|
| -
|
| - def visitor(arg, dir_name, names):
|
| - module = dir_name[len(directory_path) + 1:]
|
| - for name in names:
|
| - file_name = os.path.join(dir_name, name)
|
| - (interface, ext) = os.path.splitext(file_name)
|
| - if ext == '.idl' and not name.startswith('._'):
|
| - self.import_idl_file(file_name, import_options)
|
| - os.path.walk(directory_path, visitor, None)
|
| -
|
| - def fix_displacements(self, source):
|
| - """E.g. In W3C, something is declared on HTMLDocument but in WebKit
|
| - its on Document, so we need to mark that something in HTMLDocument
|
| - with @WebKit(via=Document). The 'via' attribute specifies the
|
| - parent interface that has the declaration."""
|
| -
|
| - for interface in self._database.GetInterfaces():
|
| - changed = False
|
| -
|
| - _logger.info('fixing displacements in %s' % interface.id)
|
| -
|
| - for parent_interface in self._get_parent_interfaces(interface):
|
| - _logger.info('scanning parent %s of %s' %
|
| - (parent_interface.id, interface.id))
|
| -
|
| - def fix_nodes(local_list, parent_list):
|
| - changed = False
|
| - parent_signatures_map = self._build_signatures_map(
|
| - parent_list)
|
| - for idl_node in local_list:
|
| - sig = self._sign(idl_node)
|
| - if sig in parent_signatures_map:
|
| - parent_member = parent_signatures_map[sig]
|
| - if (source in parent_member.annotations
|
| - and source not in idl_node.annotations
|
| - and _VIA_ANNOTATION_ATTR_NAME
|
| - not in parent_member.annotations[source]):
|
| - idl_node.annotations[source] = IDLAnnotation(
|
| - {_VIA_ANNOTATION_ATTR_NAME: parent_interface.id})
|
| - changed = True
|
| - return changed
|
| -
|
| - changed = fix_nodes(interface.constants,
|
| - parent_interface.constants) or changed
|
| - changed = fix_nodes(interface.attributes,
|
| - parent_interface.attributes) or changed
|
| - changed = fix_nodes(interface.operations,
|
| - parent_interface.operations) or changed
|
| - if changed:
|
| - _logger.info('fixed displaced declarations in %s' %
|
| - interface.id)
|
| -
|
| - def normalize_annotations(self, sources):
|
| - """Makes the IDLs less verbose by removing annotation attributes
|
| - that are identical to the ones defined at the interface level.
|
| -
|
| - Args:
|
| - sources -- list of source names to normalize."""
|
| - for interface in self._database.GetInterfaces():
|
| - _logger.debug('normalizing annotations for %s' % interface.id)
|
| - for source in sources:
|
| - if (source not in interface.annotations or
|
| - not interface.annotations[source]):
|
| - continue
|
| - top_level_annotation = interface.annotations[source]
|
| -
|
| - def normalize(idl_node):
|
| - if (source in idl_node.annotations
|
| - and idl_node.annotations[source]):
|
| - annotation = idl_node.annotations[source]
|
| - for name, value in annotation.items():
|
| - if (name in top_level_annotation
|
| - and value == top_level_annotation[name]):
|
| - del annotation[name]
|
| -
|
| - map(normalize, interface.parents)
|
| - map(normalize, interface.constants)
|
| - map(normalize, interface.attributes)
|
| - map(normalize, interface.operations)
|
|
|