blob: b1e4072865a1fea78a37d9631c44030b1093dec9 [file] [log] [blame]
# 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 adds and verifies attributes in Emboss IR.
The main entry point is normalize_and_verify(), which adds attributes and/or
verifies attributes which may have been manually entered.
"""
from front_end import attributes
from front_end import type_check
from public import ir_pb2
from util import error
from util import ir_util
from util import traverse_ir
# The "namespace" attribute is C++-back-end specific, and so should not be used
# by the front end.
_NAMESPACE = "namespace"
# 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."
# 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=attr.name.text,
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=attr.name.text,
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=attr.name.text,
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=attr.name.text))]]
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=attr.name.text,
type="a string"))]]
return []
def _is_valid_byte_order(attr, module_source_file):
"""Checks if the given attr is a valid byte_order."""
return _is_string_from_list(attr, module_source_file,
{"BigEndian", "LittleEndian", "Null"})
def _is_string_from_list(attr, module_source_file, valid_values):
"""Checks if the given attr has one of the valid_values."""
if 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=attr.name.text,
options="' or '".join(sorted(valid_values))))]]
return []
def _is_valid_text_output(attr, module_source_file):
"""Checks if the given attr is a valid text_output."""
return _is_string_from_list(attr, module_source_file, {"Emit", "Skip"})
# Attributes must be the same type no matter where they occur.
_ATTRIBUTE_TYPES = {
("", attributes.ADDRESSABLE_UNIT_SIZE): _is_constant_integer,
("", attributes.BYTE_ORDER): _is_valid_byte_order,
("", attributes.FIXED_SIZE): _is_constant_integer,
("", attributes.IS_INTEGER): _is_constant_boolean,
("", attributes.REQUIRES): _is_boolean,
("", attributes.STATIC_REQUIREMENTS): _is_boolean,
("", attributes.TEXT_OUTPUT): _is_valid_text_output,
("cpp", _NAMESPACE): _is_string,
}
_MODULE_ATTRIBUTES = {
("", attributes.BYTE_ORDER, True),
# TODO(bolms): Allow back-end-specific attributes to be specified
# externally.
("cpp", _NAMESPACE, False),
}
_BITS_ATTRIBUTES = {
("", attributes.FIXED_SIZE, False),
("", attributes.REQUIRES, False),
("", attributes.STATIC_REQUIREMENTS, False),
}
_STRUCT_ATTRIBUTES = {
("", attributes.FIXED_SIZE, False),
("", attributes.BYTE_ORDER, True),
("", attributes.REQUIRES, False),
("", attributes.STATIC_REQUIREMENTS, False),
}
_ENUM_ATTRIBUTES = {
("", attributes.STATIC_REQUIREMENTS, False),
}
_EXTERNAL_ATTRIBUTES = {
("", attributes.ADDRESSABLE_UNIT_SIZE, False),
("", attributes.FIXED_SIZE, False),
("", attributes.IS_INTEGER, False),
("", attributes.STATIC_REQUIREMENTS, False),
}
_STRUCT_PHYSICAL_FIELD_ATTRIBUTES = {
("", attributes.BYTE_ORDER, False),
("", attributes.REQUIRES, False),
("", attributes.TEXT_OUTPUT, False),
}
_STRUCT_VIRTUAL_FIELD_ATTRIBUTES = {
("", attributes.REQUIRES, False),
("", attributes.TEXT_OUTPUT, False),
}
def _construct_integer_attribute(name, value, source_location):
"""Constructs an integer Attribute with the given name and value."""
attr_value = ir_pb2.AttributeValue(
expression=ir_pb2.Expression(
constant=ir_pb2.NumericConstant(value=str(value),
source_location=source_location),
type=ir_pb2.ExpressionType(
integer=ir_pb2.IntegerType(modular_value=str(value),
modulus="infinity",
minimum_value=str(value),
maximum_value=str(value))),
source_location=source_location),
source_location=source_location)
return ir_pb2.Attribute(name=ir_pb2.Word(text=name),
value=attr_value,
source_location=source_location)
def _construct_string_attribute(name, value, source_location):
"""Constructs a string Attribute with the given name and value."""
attr_value = ir_pb2.AttributeValue(
string_constant=ir_pb2.String(text=value,
source_location=source_location),
source_location=source_location)
return ir_pb2.Attribute(name=ir_pb2.Word(text=name,
source_location=source_location),
value=attr_value,
source_location=source_location)
def _check_attributes_in_ir(ir):
"""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_pb2.EmbossIr to check.
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, _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_pb2.TypeDefinition.BYTE:
errors.extend(_check_attributes(
type_definition.attribute, _STRUCT_ATTRIBUTES, "struct '{}'".format(
type_definition.name.name.text), source_file_name))
elif type_definition.addressable_unit == ir_pb2.TypeDefinition.BIT:
errors.extend(_check_attributes(
type_definition.attribute, _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, _ENUM_ATTRIBUTES, "enum '{}'".format(
type_definition.name.name.text), source_file_name))
elif type_definition.HasField("external"):
errors.extend(_check_attributes(
type_definition.attribute, _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 = _STRUCT_VIRTUAL_FIELD_ATTRIBUTES
field_adjective = "virtual "
else:
field_attributes = _STRUCT_PHYSICAL_FIELD_ATTRIBUTES
field_adjective = ""
errors.extend(_check_attributes(
field.attribute, field_attributes,
"{}struct field '{}'".format(field_adjective, field.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_pb2.Module], check_module,
parameters={"errors": errors})
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.TypeDefinition], check_type_definition,
parameters={"errors": errors})
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.Field], check_struct_field,
parameters={"errors": errors})
return errors
def _check_attributes(attribute_list, 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_pb2.Attribute.
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.
"""
errors = []
already_seen_attributes = {}
for attr in attribute_list:
if attr.back_end.text:
attribute_name = "({}) {}".format(attr.back_end.text, attr.name.text)
else:
attribute_name = attr.name.text
if (attr.name.text, attr.is_default) in already_seen_attributes:
original_attr = already_seen_attributes[attr.name.text, attr.is_default]
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.name.text, attr.is_default] = attr
if ((attr.back_end.text, attr.name.text, attr.is_default) 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:
attribute_check = _ATTRIBUTE_TYPES[attr.back_end.text, attr.name.text]
errors.extend(attribute_check(attr, module_source_file))
return errors
def _fixed_size_of_struct_or_bits(struct, unit_size):
"""Returns size of struct in bits or None, if struct is not fixed size."""
size = 0
for field in struct.field:
if not field.HasField("location"):
# Virtual fields do not contribute to the physical size of the struct.
continue
field_start = ir_util.constant_value(field.location.start)
field_size = ir_util.constant_value(field.location.size)
if field_start is None or field_size is None:
# Technically, start + size could be constant even if start and size are
# not; e.g. if start == x and size == 10 - x, but we don't handle that
# here.
return None
# TODO(bolms): knows_own_size
# TODO(bolms): compute min/max sizes for variable-sized arrays.
field_end = field_start + field_size
if field_end >= size:
size = field_end
return size * unit_size
def _verify_size_attributes_on_structure(struct, type_definition,
source_file_name, errors):
"""Verifies size attributes on a struct or bits."""
fixed_size = _fixed_size_of_struct_or_bits(struct,
type_definition.addressable_unit)
fixed_size_attr = ir_util.get_attribute(type_definition.attribute,
attributes.FIXED_SIZE)
if not fixed_size_attr:
return
if fixed_size is None:
errors.append([error.error(
source_file_name, fixed_size_attr.source_location,
"Struct is marked as fixed size, but contains variable-location "
"fields.")])
elif ir_util.constant_value(fixed_size_attr.expression) != fixed_size:
errors.append([error.error(
source_file_name, fixed_size_attr.source_location,
"Struct is {} bits, but is marked as {} bits.".format(
fixed_size, ir_util.constant_value(fixed_size_attr.expression)))])
# TODO(bolms): remove [fixed_size]; it is superseded by $size_in_{bits,bytes}
def _add_missing_size_attributes_on_structure(struct, type_definition):
"""Adds missing size attributes on a struct."""
fixed_size = _fixed_size_of_struct_or_bits(struct,
type_definition.addressable_unit)
if fixed_size is None:
return
fixed_size_attr = ir_util.get_attribute(type_definition.attribute,
attributes.FIXED_SIZE)
if not fixed_size_attr:
# TODO(bolms): Use the offset and length of the last field as the
# source_location of the fixed_size attribute?
type_definition.attribute.extend([
_construct_integer_attribute(attributes.FIXED_SIZE, fixed_size,
type_definition.source_location)])
def _field_needs_byte_order(field, type_definition, ir):
"""Returns true if the given field needs a byte_order attribute."""
if ir_util.field_is_virtual(field):
# Virtual fields have no physical type, and thus do not need a byte order.
return False
field_type = ir_util.find_object(
ir_util.get_base_type(field.type).atomic_type.reference.canonical_name,
ir)
assert field_type is not None
assert field_type.addressable_unit != ir_pb2.TypeDefinition.NONE
return field_type.addressable_unit != type_definition.addressable_unit
def _field_may_have_null_byte_order(field, type_definition, ir):
"""Returns true if "Null" is a valid byte order for the given field."""
# If the field is one unit in length, then byte order does not matter.
if (ir_util.is_constant(field.location.size) and
ir_util.constant_value(field.location.size) == 1):
return True
unit = type_definition.addressable_unit
# Otherwise, if the field's type is either a one-unit-sized type or an array
# of a one-unit-sized type, then byte order does not matter.
if (ir_util.fixed_size_of_type_in_bits(ir_util.get_base_type(field.type), ir)
== unit):
return True
# In all other cases, byte order does matter.
return False
def _add_missing_byte_order_attribute_on_field(field, type_definition, ir,
defaults):
"""Adds missing byte_order attributes to fields that need them."""
if _field_needs_byte_order(field, type_definition, ir):
byte_order_attr = ir_util.get_attribute(field.attribute,
attributes.BYTE_ORDER)
if byte_order_attr is None:
if attributes.BYTE_ORDER in defaults:
field.attribute.extend([defaults[attributes.BYTE_ORDER]])
elif _field_may_have_null_byte_order(field, type_definition, ir):
field.attribute.extend(
[_construct_string_attribute(attributes.BYTE_ORDER, "Null",
field.source_location)])
def _add_addressable_unit_to_external(external, type_definition):
"""Sets the addressable_unit field for an external TypeDefinition."""
# Strictly speaking, addressable_unit isn't an "attribute," but it's close
# enough that it makes sense to handle it with attributes.
del external # Unused.
size = ir_util.get_integer_attribute(type_definition.attribute,
attributes.ADDRESSABLE_UNIT_SIZE)
if size == 1:
type_definition.addressable_unit = ir_pb2.TypeDefinition.BIT
elif size == 8:
type_definition.addressable_unit = ir_pb2.TypeDefinition.BYTE
# If the addressable_unit_size is not in (1, 8), it will be caught by
# _verify_addressable_unit_attribute_on_external, below.
def _verify_byte_order_attribute_on_field(field, type_definition,
source_file_name, ir, errors):
"""Verifies the byte_order attribute on the given field."""
byte_order_attr = ir_util.get_attribute(field.attribute,
attributes.BYTE_ORDER)
field_needs_byte_order = _field_needs_byte_order(field, type_definition, ir)
if byte_order_attr and not field_needs_byte_order:
errors.append([error.error(
source_file_name, byte_order_attr.source_location,
"Attribute 'byte_order' not allowed on field which is not byte order "
"dependent.")])
if not byte_order_attr and field_needs_byte_order:
errors.append([error.error(
source_file_name, field.source_location,
"Attribute 'byte_order' required on field which is byte order "
"dependent.")])
if (byte_order_attr and byte_order_attr.string_constant.text == "Null" and
not _field_may_have_null_byte_order(field, type_definition, ir)):
errors.append([error.error(
source_file_name, byte_order_attr.source_location,
"Attribute 'byte_order' may only be 'Null' for one-byte fields.")])
def _verify_requires_attribute_on_field(field, source_file_name, ir, errors):
"""Verifies that [requires] is valid on the given field."""
requires_attr = ir_util.get_attribute(field.attribute, attributes.REQUIRES)
if not requires_attr:
return
if ir_util.field_is_virtual(field):
field_expression_type = field.read_transform.type
else:
if not field.type.HasField("atomic_type"):
errors.append([
error.error(source_file_name, requires_attr.source_location,
"Attribute 'requires' is only allowed on integer, "
"enumeration, or boolean fields, not arrays."),
error.note(source_file_name, field.type.source_location,
"Field type."),
])
return
field_type = ir_util.find_object(field.type.atomic_type.reference, ir)
assert field_type, "Field type should be non-None after name resolution."
field_expression_type = (
type_check.unbounded_expression_type_for_physical_type(field_type))
if field_expression_type.WhichOneof("type") not in (
"integer", "enumeration", "boolean"):
errors.append([error.error(
source_file_name, requires_attr.source_location,
"Attribute 'requires' is only allowed on integer, enumeration, or "
"boolean fields.")])
def _verify_addressable_unit_attribute_on_external(external, type_definition,
source_file_name, errors):
"""Verifies the addressable_unit_size attribute on an external."""
del external # Unused.
addressable_unit_size_attr = ir_util.get_integer_attribute(
type_definition.attribute, attributes.ADDRESSABLE_UNIT_SIZE)
if addressable_unit_size_attr is None:
errors.append([error.error(
source_file_name, type_definition.source_location,
"Expected '{}' attribute for external type.".format(
attributes.ADDRESSABLE_UNIT_SIZE))])
elif addressable_unit_size_attr not in (1, 8):
errors.append([
error.error(source_file_name, type_definition.source_location,
"Only values '1' (bit) and '8' (byte) are allowed for the "
"'{}' attribute".format(attributes.ADDRESSABLE_UNIT_SIZE))
])
def _gather_default_attributes(obj, defaults):
defaults = defaults.copy()
for attr in obj.attribute:
if attr.is_default:
defaulted_attr = ir_pb2.Attribute()
defaulted_attr.CopyFrom(attr)
defaulted_attr.is_default = False
defaults[attr.name.text] = defaulted_attr
return {"defaults": defaults}
def _add_missing_attributes_on_ir(ir):
"""Adds missing attributes in a complete IR."""
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.External], _add_addressable_unit_to_external)
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.Structure], _add_missing_size_attributes_on_structure,
incidental_actions={
ir_pb2.Module: _gather_default_attributes,
ir_pb2.TypeDefinition: _gather_default_attributes,
ir_pb2.Field: _gather_default_attributes,
},
parameters={"defaults": {}})
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.Field], _add_missing_byte_order_attribute_on_field,
incidental_actions={
ir_pb2.Module: _gather_default_attributes,
ir_pb2.TypeDefinition: _gather_default_attributes,
ir_pb2.Field: _gather_default_attributes,
},
parameters={"defaults": {}})
return []
def _verify_field_attributes(field, type_definition, source_file_name, ir,
errors):
_verify_byte_order_attribute_on_field(field, type_definition,
source_file_name, ir, errors)
_verify_requires_attribute_on_field(field, source_file_name, ir, errors)
def _verify_attributes_on_ir(ir):
"""Verifies attributes in a complete IR."""
errors = []
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.Structure], _verify_size_attributes_on_structure,
parameters={"errors": errors})
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.External], _verify_addressable_unit_attribute_on_external,
parameters={"errors": errors})
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.Field], _verify_field_attributes,
parameters={"errors": errors})
return errors
def normalize_and_verify(ir):
"""Performs various normalizations and verifications on ir.
Checks for duplicate attributes.
Adds fixed_size_in_bits and addressable_unit_size attributes to types when
they are missing, and checks their correctness when they are not missing.
Arguments:
ir: The IR object to normalize.
Returns:
A list of validation errors, or an empty list if no errors were encountered.
"""
errors = _check_attributes_in_ir(ir)
if errors:
return errors
_add_missing_attributes_on_ir(ir)
return _verify_attributes_on_ir(ir)