| # Copyright 2019 Google LLC |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Module which verifies attributes in an Emboss IR. |
| |
| The main entry point is check_attributes_in_ir(), which checks attributes in an |
| IR. |
| """ |
| |
| from compiler.util import error |
| from compiler.util import ir_data |
| from compiler.util import ir_data_utils |
| from compiler.util import ir_util |
| from compiler.util import traverse_ir |
| |
| |
| # Error messages used by multiple attribute type checkers. |
| _BAD_TYPE_MESSAGE = "Attribute '{name}' must have {type} value." |
| _MUST_BE_CONSTANT_MESSAGE = "Attribute '{name}' must have a constant value." |
| |
| |
| def _attribute_name_for_errors(attr): |
| if ir_data_utils.reader(attr).back_end.text: |
| return f"({attr.back_end.text}) {attr.name.text}" |
| else: |
| return attr.name.text |
| |
| |
| # Attribute type checkers |
| def _is_constant_boolean(attr, module_source_file): |
| """Checks if the given attr is a constant boolean.""" |
| if not attr.value.expression.type.boolean.HasField("value"): |
| return [ |
| [ |
| error.error( |
| module_source_file, |
| attr.value.source_location, |
| _BAD_TYPE_MESSAGE.format( |
| name=_attribute_name_for_errors(attr), type="a constant boolean" |
| ), |
| ) |
| ] |
| ] |
| return [] |
| |
| |
| def _is_boolean(attr, module_source_file): |
| """Checks if the given attr is a boolean.""" |
| if attr.value.expression.type.WhichOneof("type") != "boolean": |
| return [ |
| [ |
| error.error( |
| module_source_file, |
| attr.value.source_location, |
| _BAD_TYPE_MESSAGE.format( |
| name=_attribute_name_for_errors(attr), type="a boolean" |
| ), |
| ) |
| ] |
| ] |
| return [] |
| |
| |
| def _is_constant_integer(attr, module_source_file): |
| """Checks if the given attr is an integer constant expression.""" |
| if ( |
| not attr.value.HasField("expression") |
| or attr.value.expression.type.WhichOneof("type") != "integer" |
| ): |
| return [ |
| [ |
| error.error( |
| module_source_file, |
| attr.value.source_location, |
| _BAD_TYPE_MESSAGE.format( |
| name=_attribute_name_for_errors(attr), type="an integer" |
| ), |
| ) |
| ] |
| ] |
| if not ir_util.is_constant(attr.value.expression): |
| return [ |
| [ |
| error.error( |
| module_source_file, |
| attr.value.source_location, |
| _MUST_BE_CONSTANT_MESSAGE.format( |
| name=_attribute_name_for_errors(attr) |
| ), |
| ) |
| ] |
| ] |
| return [] |
| |
| |
| def _is_string(attr, module_source_file): |
| """Checks if the given attr is a string.""" |
| if not attr.value.HasField("string_constant"): |
| return [ |
| [ |
| error.error( |
| module_source_file, |
| attr.value.source_location, |
| _BAD_TYPE_MESSAGE.format( |
| name=_attribute_name_for_errors(attr), type="a string" |
| ), |
| ) |
| ] |
| ] |
| return [] |
| |
| |
| # Provide more readable names for these functions when used in attribute type |
| # specifiers. |
| BOOLEAN_CONSTANT = _is_constant_boolean |
| BOOLEAN = _is_boolean |
| INTEGER_CONSTANT = _is_constant_integer |
| STRING = _is_string |
| |
| |
| def string_from_list(valid_values): |
| """Checks if the given attr has one of the valid_values.""" |
| |
| def _string_from_list(attr, module_source_file): |
| if ir_data_utils.reader(attr).value.string_constant.text not in valid_values: |
| return [ |
| [ |
| error.error( |
| module_source_file, |
| attr.value.source_location, |
| "Attribute '{name}' must be '{options}'.".format( |
| name=_attribute_name_for_errors(attr), |
| options="' or '".join(sorted(valid_values)), |
| ), |
| ) |
| ] |
| ] |
| return [] |
| |
| return _string_from_list |
| |
| |
| def check_attributes_in_ir( |
| ir, |
| *, |
| back_end=None, |
| types=None, |
| module_attributes=None, |
| struct_attributes=None, |
| bits_attributes=None, |
| enum_attributes=None, |
| enum_value_attributes=None, |
| external_attributes=None, |
| structure_virtual_field_attributes=None, |
| structure_physical_field_attributes=None, |
| ): |
| """Performs basic checks on all attributes in the given ir. |
| |
| This function calls _check_attributes on each attribute list in ir. |
| |
| Arguments: |
| ir: An ir_data.EmbossIr to check. |
| back_end: A string specifying the attribute qualifier to check (such as |
| `cpp` for `[(cpp) namespace = "foo"]`), or None to check unqualified |
| attributes. |
| |
| Attributes with a different qualifier will not be checked. |
| types: A map from attribute names to validators, such as: |
| { |
| "maximum_bits": attribute_util.INTEGER_CONSTANT, |
| "requires": attribute_util.BOOLEAN, |
| } |
| module_attributes: A set of (attribute_name, is_default) tuples specifying |
| the attributes that are allowed at module scope. |
| struct_attributes: A set of (attribute_name, is_default) tuples specifying |
| the attributes that are allowed at `struct` scope. |
| bits_attributes: A set of (attribute_name, is_default) tuples specifying |
| the attributes that are allowed at `bits` scope. |
| enum_attributes: A set of (attribute_name, is_default) tuples specifying |
| the attributes that are allowed at `enum` scope. |
| enum_value_attributes: A set of (attribute_name, is_default) tuples |
| specifying the attributes that are allowed at the scope of enum values. |
| external_attributes: A set of (attribute_name, is_default) tuples |
| specifying the attributes that are allowed at `external` scope. |
| structure_virtual_field_attributes: A set of (attribute_name, is_default) |
| tuples specifying the attributes that are allowed at the scope of |
| virtual fields (`let` fields) in structures (both `struct` and `bits`). |
| structure_physical_field_attributes: A set of (attribute_name, is_default) |
| tuples specifying the attributes that are allowed at the scope of |
| physical fields in structures (both `struct` and `bits`). |
| |
| Returns: |
| A list of lists of error.error, or an empty list if there were no errors. |
| """ |
| |
| def check_module(module, errors): |
| errors.extend( |
| _check_attributes( |
| module.attribute, |
| types, |
| back_end, |
| module_attributes, |
| "module '{}'".format(module.source_file_name), |
| module.source_file_name, |
| ) |
| ) |
| |
| def check_type_definition(type_definition, source_file_name, errors): |
| if type_definition.HasField("structure"): |
| if type_definition.addressable_unit == ir_data.AddressableUnit.BYTE: |
| errors.extend( |
| _check_attributes( |
| type_definition.attribute, |
| types, |
| back_end, |
| struct_attributes, |
| "struct '{}'".format(type_definition.name.name.text), |
| source_file_name, |
| ) |
| ) |
| elif type_definition.addressable_unit == ir_data.AddressableUnit.BIT: |
| errors.extend( |
| _check_attributes( |
| type_definition.attribute, |
| types, |
| back_end, |
| bits_attributes, |
| "bits '{}'".format(type_definition.name.name.text), |
| source_file_name, |
| ) |
| ) |
| else: |
| assert False, "Unexpected addressable_unit '{}'".format( |
| type_definition.addressable_unit |
| ) |
| elif type_definition.HasField("enumeration"): |
| errors.extend( |
| _check_attributes( |
| type_definition.attribute, |
| types, |
| back_end, |
| enum_attributes, |
| "enum '{}'".format(type_definition.name.name.text), |
| source_file_name, |
| ) |
| ) |
| elif type_definition.HasField("external"): |
| errors.extend( |
| _check_attributes( |
| type_definition.attribute, |
| types, |
| back_end, |
| external_attributes, |
| "external '{}'".format(type_definition.name.name.text), |
| source_file_name, |
| ) |
| ) |
| |
| def check_struct_field(field, source_file_name, errors): |
| if ir_util.field_is_virtual(field): |
| field_attributes = structure_virtual_field_attributes |
| field_adjective = "virtual " |
| else: |
| field_attributes = structure_physical_field_attributes |
| field_adjective = "" |
| errors.extend( |
| _check_attributes( |
| field.attribute, |
| types, |
| back_end, |
| field_attributes, |
| "{}struct field '{}'".format(field_adjective, field.name.name.text), |
| source_file_name, |
| ) |
| ) |
| |
| def check_enum_value(value, source_file_name, errors): |
| errors.extend( |
| _check_attributes( |
| value.attribute, |
| types, |
| back_end, |
| enum_value_attributes, |
| "enum value '{}'".format(value.name.name.text), |
| source_file_name, |
| ) |
| ) |
| |
| errors = [] |
| # TODO(bolms): Add a check that only known $default'ed attributes are |
| # used. |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_data.Module], check_module, parameters={"errors": errors} |
| ) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, |
| [ir_data.TypeDefinition], |
| check_type_definition, |
| parameters={"errors": errors}, |
| ) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_data.Field], check_struct_field, parameters={"errors": errors} |
| ) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_data.EnumValue], check_enum_value, parameters={"errors": errors} |
| ) |
| return errors |
| |
| |
| def _check_attributes( |
| attribute_list, types, back_end, attribute_specs, context_name, module_source_file |
| ): |
| """Performs basic checks on the given list of attributes. |
| |
| Checks the given attribute_list for duplicates, unknown attributes, attributes |
| with incorrect type, and attributes whose values are not constant. |
| |
| Arguments: |
| attribute_list: An iterable of ir_data.Attribute. |
| back_end: The qualifier for attributes to check, or None. |
| attribute_specs: A dict of attribute names to _Attribute structures |
| specifying the allowed attributes. |
| context_name: A name for the context of these attributes, such as "struct |
| 'Foo'" or "module 'm.emb'". Used in error messages. |
| module_source_file: The value of module.source_file_name from the module |
| containing 'attribute_list'. Used in error messages. |
| |
| Returns: |
| A list of lists of error.Errors. An empty list indicates no errors were |
| found. |
| """ |
| if attribute_specs is None: |
| attribute_specs = [] |
| errors = [] |
| already_seen_attributes = {} |
| for attr in attribute_list: |
| field_checker = ir_data_utils.reader(attr) |
| if field_checker.back_end.text: |
| if attr.back_end.text != back_end: |
| continue |
| else: |
| if back_end is not None: |
| continue |
| attribute_name = _attribute_name_for_errors(attr) |
| attr_key = (field_checker.name.text, field_checker.is_default) |
| if attr_key in already_seen_attributes: |
| original_attr = already_seen_attributes[attr_key] |
| errors.append( |
| [ |
| error.error( |
| module_source_file, |
| attr.source_location, |
| "Duplicate attribute '{}'.".format(attribute_name), |
| ), |
| error.note( |
| module_source_file, |
| original_attr.source_location, |
| "Original attribute", |
| ), |
| ] |
| ) |
| continue |
| already_seen_attributes[attr_key] = attr |
| |
| if attr_key not in attribute_specs: |
| if attr.is_default: |
| error_message = "Attribute '{}' may not be defaulted on {}.".format( |
| attribute_name, context_name |
| ) |
| else: |
| error_message = "Unknown attribute '{}' on {}.".format( |
| attribute_name, context_name |
| ) |
| errors.append( |
| [ |
| error.error( |
| module_source_file, attr.name.source_location, error_message |
| ) |
| ] |
| ) |
| else: |
| errors.extend(types[attr.name.text](attr, module_source_file)) |
| return errors |
| |
| |
| def gather_default_attributes(obj, defaults): |
| """Gathers default attributes for an IR object |
| |
| This is designed to be able to be used as-is as an incidental action in an IR |
| traversal to accumulate defaults for child nodes. |
| |
| Arguments: |
| defaults: A dict of `{ "defaults": { attr.name.text: attr } }` |
| |
| Returns: |
| A dict of `{ "defaults": { attr.name.text: attr } }` with any defaults |
| provided by `obj` added/overridden. |
| """ |
| defaults = defaults.copy() |
| for attr in obj.attribute: |
| if attr.is_default: |
| defaulted_attr = ir_data_utils.copy(attr) |
| defaulted_attr.is_default = False |
| defaults[attr.name.text] = defaulted_attr |
| return {"defaults": defaults} |