blob: d864364040b880352214dca63ced68c2b7dd7f71 [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 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_pb2
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 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 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_pb2.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_pb2.TypeDefinition.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_pb2.TypeDefinition.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_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})
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.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_pb2.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:
if attr.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)
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.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:
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_pb2.Attribute()
defaulted_attr.CopyFrom(attr)
defaulted_attr.is_default = False
defaults[attr.name.text] = defaulted_attr
return {"defaults": defaults}