Factor back-end attribute checking into the back end. (#80)
Factor back-end attribute checking into the back end.
This change splits up `front_end/attribute_checker.py`, moving the
generic parts to a new file `util/attribute_util.py`, and moving the
back-end-specific parts to `back_end/cpp/header_generator.py`.
Some tests from `front_end/attribute_checker_test.py` were moved to
a new test suite, `header_generator_test.py`. There should probably
be a `util/attribute_checker_test.py`, but for now the old tests
provide sufficient coverage.
As a result of moving some attribute checking into the back end,
`generate_header()` can now return errors. A future change should
convert some of the `assert` statements in the same file into error
returns.
In order for the `emboss_codegen_cpp.py` driver to properly display
errors, the original source code of the `.emb` is now included
verbatim in the IR, increasing the IR size by about 3%.
This change does still enforce some minimal checking of back end
attributes: the back end must be listed in the new
`[expected_back_ends]` attribute (default value `"cpp"`), or it is
considered to be an error. This change does not document the
`[expected_back_ends]` attribute because it is not currently useful for
end users.
diff --git a/compiler/back_end/cpp/BUILD b/compiler/back_end/cpp/BUILD
index c9e879c..2280c36 100644
--- a/compiler/back_end/cpp/BUILD
+++ b/compiler/back_end/cpp/BUILD
@@ -42,12 +42,22 @@
],
deps = [
"//compiler/back_end/util:code_template",
+ "//compiler/util:attribute_util",
"//compiler/util:ir_pb2",
"//compiler/util:ir_util",
"//compiler/util:name_conversion",
],
)
+py_test(
+ name = "header_generator_test",
+ srcs = ["header_generator_test.py"],
+ deps = [
+ ":header_generator",
+ "//compiler/front_end:glue",
+ ],
+)
+
emboss_cc_test(
name = "span_se_log_file_status_emb_generated_code_test",
srcs = [
diff --git a/compiler/back_end/cpp/emboss_codegen_cpp.py b/compiler/back_end/cpp/emboss_codegen_cpp.py
index 73a1450..77bcb84 100644
--- a/compiler/back_end/cpp/emboss_codegen_cpp.py
+++ b/compiler/back_end/cpp/emboss_codegen_cpp.py
@@ -21,9 +21,11 @@
from __future__ import print_function
import argparse
+import os
import sys
from compiler.back_end.cpp import header_generator
+from compiler.util import error
from compiler.util import ir_pb2
@@ -38,16 +40,35 @@
type=str,
help="Write header to file. If not specified, write " +
"header to stdout.")
+ parser.add_argument("--color-output",
+ default="if_tty",
+ choices=["always", "never", "if_tty", "auto"],
+ help="Print error messages using color. 'auto' is a "
+ "synonym for 'if_tty'.")
return parser.parse_args(argv[1:])
+def _show_errors(errors, ir, flags):
+ """Prints errors with source code snippets."""
+ source_codes = {}
+ for module in ir.module:
+ source_codes[module.source_file_name] = module.source_text
+ use_color = (flags.color_output == "always" or
+ (flags.color_output in ("auto", "if_tty") and
+ os.isatty(sys.stderr.fileno())))
+ print(error.format_errors(errors, source_codes, use_color), file=sys.stderr)
+
+
def main(flags):
if flags.input_file:
with open(flags.input_file) as f:
ir = ir_pb2.EmbossIr.from_json(f.read())
else:
ir = ir_pb2.EmbossIr.from_json(sys.stdin.read())
- header = header_generator.generate_header(ir)
+ header, errors = header_generator.generate_header(ir)
+ if errors:
+ _show_errors(errors, ir, flags)
+ return 1
if flags.output_file:
with open(flags.output_file, "w") as f:
f.write(header)
@@ -56,5 +77,5 @@
return 0
-if __name__ == "__main__":
+if __name__ == '__main__':
sys.exit(main(_parse_command_line(sys.argv)))
diff --git a/compiler/back_end/cpp/header_generator.py b/compiler/back_end/cpp/header_generator.py
index 4fc7890..e467284 100644
--- a/compiler/back_end/cpp/header_generator.py
+++ b/compiler/back_end/cpp/header_generator.py
@@ -23,6 +23,7 @@
import re
from compiler.back_end.util import code_template
+from compiler.util import attribute_util
from compiler.util import ir_pb2
from compiler.util import ir_util
from compiler.util import name_conversion
@@ -1309,9 +1310,18 @@
ir: An EmbossIr of the module.
Returns:
- A string containing the text of a C++ header which implements Views for the
- types in the Emboss module.
+ A tuple of (header, errors), where `header` is either a string containing
+ the text of a C++ header which implements Views for the types in the Emboss
+ module, or None, and `errors` is a possibly-empty list of error messages to
+ display to the user.
"""
+ errors = attribute_util.check_attributes_in_ir(
+ ir,
+ back_end="cpp",
+ types={"namespace": attribute_util.STRING},
+ module_attributes={("namespace", False)})
+ if errors:
+ return None, errors
type_declarations = []
type_definitions = []
method_definitions = []
@@ -1332,4 +1342,4 @@
_TEMPLATES.outline,
includes=includes,
body=body,
- header_guard=_generate_header_guard(ir.module[0].source_file_name))
+ header_guard=_generate_header_guard(ir.module[0].source_file_name)), []
diff --git a/compiler/back_end/cpp/header_generator_test.py b/compiler/back_end/cpp/header_generator_test.py
new file mode 100644
index 0000000..e057c0d
--- /dev/null
+++ b/compiler/back_end/cpp/header_generator_test.py
@@ -0,0 +1,53 @@
+# Copyright 2023 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.
+
+"""Tests for attribute_checker.py."""
+
+import unittest
+from compiler.back_end.cpp import header_generator
+from compiler.front_end import glue
+from compiler.util import error
+from compiler.util import ir_pb2
+from compiler.util import ir_util
+
+
+def _make_ir_from_emb(emb_text, name="m.emb"):
+ ir, unused_debug_info, errors = glue.parse_emboss_file(
+ name,
+ test_util.dict_file_reader({name: emb_text}))
+ assert not errors
+ return ir
+
+
+class NormalizeIrTest(unittest.TestCase):
+
+ def test_accepts_string_attribute(self):
+ ir = _make_ir_from_emb('[(cpp) namespace: "foo"]\n')
+ self.assertEqual([], header_generator.generate_header(ir)[1])
+
+ def test_rejects_wrong_type_for_string_attribute(self):
+ ir = _make_ir_from_emb("[(cpp) namespace: 9]\n")
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error("m.emb", attr.value.source_location,
+ "Attribute '(cpp) namespace' must have a string value.")
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_emboss_internal_attribute_with_back_end_specifier(self):
+ ir = _make_ir_from_emb('[(cpp) byte_order: "LittleEndian"]\n')
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error("m.emb", attr.name.source_location,
+ "Unknown attribute '(cpp) byte_order' on module 'm.emb'.")
+ ]], attribute_checker.normalize_and_verify(ir))
diff --git a/compiler/front_end/BUILD b/compiler/front_end/BUILD
index 14a4696..f8d8484 100644
--- a/compiler/front_end/BUILD
+++ b/compiler/front_end/BUILD
@@ -213,7 +213,6 @@
name = "symbol_resolver_test",
srcs = ["symbol_resolver_test.py"],
python_version = "PY3",
- shard_count = 8,
deps = [
":glue",
":symbol_resolver",
@@ -239,7 +238,6 @@
name = "write_inference_test",
srcs = ["write_inference_test.py"],
python_version = "PY3",
- shard_count = 8,
deps = [
":glue",
":test_util",
@@ -254,8 +252,9 @@
deps = [
":attributes",
":type_check",
- "//compiler/util:ir_pb2",
+ "//compiler/util:attribute_util",
"//compiler/util:error",
+ "//compiler/util:ir_pb2",
"//compiler/util:ir_util",
"//compiler/util:traverse_ir",
],
@@ -272,7 +271,6 @@
timeout = "long",
srcs = ["attribute_checker_test.py"],
python_version = "PY3",
- shard_count = 16,
deps = [
":attribute_checker",
":glue",
@@ -299,7 +297,6 @@
name = "type_check_test",
srcs = ["type_check_test.py"],
python_version = "PY3",
- shard_count = 8,
deps = [
":glue",
":test_util",
@@ -326,7 +323,6 @@
name = "expression_bounds_test",
srcs = ["expression_bounds_test.py"],
python_version = "PY3",
- shard_count = 4,
deps = [
":expression_bounds",
":glue",
@@ -353,7 +349,6 @@
name = "constraints_test",
srcs = ["constraints_test.py"],
python_version = "PY3",
- shard_count = 8,
deps = [
":constraints",
":glue",
@@ -377,7 +372,6 @@
name = "dependency_checker_test",
srcs = ["dependency_checker_test.py"],
python_version = "PY3",
- shard_count = 8,
deps = [
":dependency_checker",
":glue",
diff --git a/compiler/front_end/attribute_checker.py b/compiler/front_end/attribute_checker.py
index 27e6722..21c6b22 100644
--- a/compiler/front_end/attribute_checker.py
+++ b/compiler/front_end/attribute_checker.py
@@ -18,143 +18,88 @@
verifies attributes which may have been manually entered.
"""
+import re
+
from compiler.front_end import attributes
from compiler.front_end import type_check
+from compiler.util import attribute_util
from compiler.util import error
from compiler.util import ir_pb2
from compiler.util import ir_util
from compiler.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."
-
# Default value for maximum_bits on an `enum`.
_DEFAULT_ENUM_MAXIMUM_BITS = 64
+# Default value for expected_back_ends -- mostly for legacy
+_DEFAULT_BACK_ENDS = "cpp"
# 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"))]]
+_VALID_BYTE_ORDER = attribute_util.string_from_list(
+ {"BigEndian", "LittleEndian", "Null"})
+_VALID_TEXT_OUTPUT = attribute_util.string_from_list({"Emit", "Skip"})
+
+
+def _valid_back_ends(attr, module_source_file):
+ if not re.match(
+ r"^(?:\s*[a-z][a-z0-9_]*\s*(?:,\s*[a-z][a-z0-9_]*\s*)*,?)?\s*$",
+ attr.value.string_constant.text):
+ return [[error.error(
+ module_source_file,
+ attr.value.source_location,
+ "Attribute '{name}' must be a comma-delimited list of back end "
+ "specifiers (like \"cpp, proto\")), not \"{value}\".".format(
+ name=attr.name.text,
+ value=attr.value.string_constant.text))]]
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.ENUM_MAXIMUM_BITS): _is_constant_integer,
- ("", attributes.FIXED_SIZE): _is_constant_integer,
- ("", attributes.IS_INTEGER): _is_constant_boolean,
- ("", attributes.IS_SIGNED): _is_constant_boolean,
- ("", attributes.REQUIRES): _is_boolean,
- ("", attributes.STATIC_REQUIREMENTS): _is_boolean,
- ("", attributes.TEXT_OUTPUT): _is_valid_text_output,
- ("cpp", _NAMESPACE): _is_string,
+ attributes.ADDRESSABLE_UNIT_SIZE: attribute_util.INTEGER_CONSTANT,
+ attributes.BYTE_ORDER: _VALID_BYTE_ORDER,
+ attributes.ENUM_MAXIMUM_BITS: attribute_util.INTEGER_CONSTANT,
+ attributes.FIXED_SIZE: attribute_util.INTEGER_CONSTANT,
+ attributes.IS_INTEGER: attribute_util.BOOLEAN_CONSTANT,
+ attributes.IS_SIGNED: attribute_util.BOOLEAN_CONSTANT,
+ attributes.REQUIRES: attribute_util.BOOLEAN,
+ attributes.STATIC_REQUIREMENTS: attribute_util.BOOLEAN,
+ attributes.TEXT_OUTPUT: _VALID_TEXT_OUTPUT,
+ attributes.BACK_ENDS: _valid_back_ends,
}
_MODULE_ATTRIBUTES = {
- ("", attributes.BYTE_ORDER, True),
- # TODO(bolms): Allow back-end-specific attributes to be specified
- # externally.
- ("cpp", _NAMESPACE, False),
+ (attributes.BYTE_ORDER, True),
+ (attributes.BACK_ENDS, False),
}
_BITS_ATTRIBUTES = {
- ("", attributes.FIXED_SIZE, False),
- ("", attributes.REQUIRES, False),
+ (attributes.FIXED_SIZE, False),
+ (attributes.REQUIRES, False),
}
_STRUCT_ATTRIBUTES = {
- ("", attributes.FIXED_SIZE, False),
- ("", attributes.BYTE_ORDER, True),
- ("", attributes.REQUIRES, False),
+ (attributes.FIXED_SIZE, False),
+ (attributes.BYTE_ORDER, True),
+ (attributes.REQUIRES, False),
}
_ENUM_ATTRIBUTES = {
- ("", attributes.ENUM_MAXIMUM_BITS, False),
- ("", attributes.IS_SIGNED, False),
+ (attributes.ENUM_MAXIMUM_BITS, False),
+ (attributes.IS_SIGNED, False),
}
_EXTERNAL_ATTRIBUTES = {
- ("", attributes.ADDRESSABLE_UNIT_SIZE, False),
- ("", attributes.FIXED_SIZE, False),
- ("", attributes.IS_INTEGER, False),
- ("", attributes.STATIC_REQUIREMENTS, False),
+ (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),
+ (attributes.BYTE_ORDER, False),
+ (attributes.REQUIRES, False),
+ (attributes.TEXT_OUTPUT, False),
}
_STRUCT_VIRTUAL_FIELD_ATTRIBUTES = {
- ("", attributes.REQUIRES, False),
- ("", attributes.TEXT_OUTPUT, False),
+ (attributes.REQUIRES, False),
+ (attributes.TEXT_OUTPUT, False),
}
@@ -204,129 +149,6 @@
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
@@ -431,6 +253,24 @@
field.source_location)])
+def _add_missing_back_ends_to_module(module):
+ """Sets the expected_back_ends attribute for a module, if not already set."""
+ back_ends_attr = ir_util.get_attribute(module.attribute, attributes.BACK_ENDS)
+ if back_ends_attr is None:
+ module.attribute.extend(
+ [_construct_string_attribute(attributes.BACK_ENDS, _DEFAULT_BACK_ENDS,
+ module.source_location)])
+
+
+def _gather_expected_back_ends(module):
+ """Captures the expected_back_ends attribute for `module`."""
+ back_ends_attr = ir_util.get_attribute(module.attribute, attributes.BACK_ENDS)
+ back_ends_str = back_ends_attr.string_constant.text
+ return {
+ "expected_back_ends": {x.strip() for x in back_ends_str.split(",")} | {""}
+ }
+
+
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
@@ -571,6 +411,8 @@
def _add_missing_attributes_on_ir(ir):
"""Adds missing attributes in a complete IR."""
traverse_ir.fast_traverse_ir_top_down(
+ ir, [ir_pb2.Module], _add_missing_back_ends_to_module)
+ 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.Enum], _add_missing_width_and_sign_attributes_on_enum)
@@ -600,10 +442,35 @@
_verify_requires_attribute_on_field(field, source_file_name, ir, errors)
+def _verify_back_end_attributes(attribute, expected_back_ends, source_file_name,
+ ir, errors):
+ back_end_text = attribute.back_end.text
+ if back_end_text not in expected_back_ends:
+ expected_back_ends_for_error = expected_back_ends - {""}
+ errors.append([error.error(
+ source_file_name, attribute.back_end.source_location,
+ "Back end specifier '{back_end}' does not match any expected back end "
+ "specifier for this file: '{expected_back_ends}'. Add or update the "
+ "'[expected_back_ends: \"{new_expected_back_ends}\"]' attribute at the "
+ "file level if this back end specifier is intentional.".format(
+ back_end=attribute.back_end.text,
+ expected_back_ends="', '".join(
+ sorted(expected_back_ends_for_error)),
+ new_expected_back_ends=", ".join(
+ sorted(expected_back_ends_for_error | {back_end_text})),
+ ))])
+
+
def _verify_attributes_on_ir(ir):
"""Verifies attributes in a complete IR."""
errors = []
traverse_ir.fast_traverse_ir_top_down(
+ ir, [ir_pb2.Attribute], _verify_back_end_attributes,
+ incidental_actions={
+ ir_pb2.Module: _gather_expected_back_ends,
+ },
+ parameters={"errors": 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(
@@ -632,7 +499,16 @@
Returns:
A list of validation errors, or an empty list if no errors were encountered.
"""
- errors = _check_attributes_in_ir(ir)
+ errors = attribute_util.check_attributes_in_ir(
+ ir,
+ types=_ATTRIBUTE_TYPES,
+ module_attributes=_MODULE_ATTRIBUTES,
+ struct_attributes=_STRUCT_ATTRIBUTES,
+ bits_attributes=_BITS_ATTRIBUTES,
+ enum_attributes=_ENUM_ATTRIBUTES,
+ external_attributes=_EXTERNAL_ATTRIBUTES,
+ structure_virtual_field_attributes=_STRUCT_VIRTUAL_FIELD_ATTRIBUTES,
+ structure_physical_field_attributes=_STRUCT_PHYSICAL_FIELD_ATTRIBUTES)
if errors:
return errors
_add_missing_attributes_on_ir(ir)
diff --git a/compiler/front_end/attribute_checker_test.py b/compiler/front_end/attribute_checker_test.py
index efae0e6..def86c6 100644
--- a/compiler/front_end/attribute_checker_test.py
+++ b/compiler/front_end/attribute_checker_test.py
@@ -267,22 +267,6 @@
]],
attribute_checker.normalize_and_verify(ir))
- def test_accepts_string_attribute(self):
- ir = _make_ir_from_emb('[(cpp) namespace: "foo"]\n')
- self.assertEqual([], attribute_checker.normalize_and_verify(ir))
-
- def test_rejects_wrong_type_for_string_attribute(self):
- ir = _make_ir_from_emb("[(cpp) namespace: 9]\n")
- attr = ir.module[0].attribute[0]
- self.assertEqual([[
- error.error("m.emb", attr.value.source_location,
- "Attribute 'namespace' must have a string value.")
- ]], attribute_checker.normalize_and_verify(ir))
-
- def test_accepts_back_end_qualified_attribute(self):
- ir = _make_ir_from_emb('[(cpp) namespace: "abc"]\n')
- self.assertEqual([], attribute_checker.normalize_and_verify(ir))
-
def test_rejects_attribute_missing_required_back_end_specifier(self):
ir = _make_ir_from_emb('[namespace: "abc"]\n')
attr = ir.module[0].attribute[0]
@@ -291,22 +275,63 @@
"Unknown attribute 'namespace' on module 'm.emb'.")
]], attribute_checker.normalize_and_verify(ir))
- def test_rejects_attribute_with_wrong_back_end_specifier(self):
- ir = _make_ir_from_emb('[(c) namespace: "abc"]\n')
+ def test_accepts_attribute_with_default_known_back_end_specifier(self):
+ ir = _make_ir_from_emb('[(cpp) namespace: "abc"]\n')
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+
+ def test_rejects_attribute_with_specified_back_end_specifier(self):
+ ir = _make_ir_from_emb('[(c) namespace: "abc"]\n'
+ '[expected_back_ends: "c, cpp"]\n')
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+
+ def test_rejects_cpp_backend_attribute_when_not_in_expected_back_ends(self):
+ ir = _make_ir_from_emb('[(cpp) namespace: "abc"]\n'
+ '[expected_back_ends: "c"]\n')
attr = ir.module[0].attribute[0]
+ self.maxDiff = 200000
self.assertEqual([[
- error.error("m.emb", attr.name.source_location,
- "Unknown attribute '(c) namespace' on module 'm.emb'.")
+ error.error(
+ "m.emb", attr.back_end.source_location,
+ "Back end specifier 'cpp' does not match any expected back end "
+ "specifier for this file: 'c'. Add or update the "
+ "'[expected_back_ends: \"c, cpp\"]' attribute at the file level if "
+ "this back end specifier is intentional.")
]], attribute_checker.normalize_and_verify(ir))
- def test_rejects_emboss_internal_attribute_with_back_end_specifier(self):
- ir = _make_ir_from_emb('[(cpp) byte_order: "LittleEndian"]\n')
+ def test_rejects_expected_back_ends_with_bad_back_end(self):
+ ir = _make_ir_from_emb('[expected_back_ends: "c++"]\n')
attr = ir.module[0].attribute[0]
self.assertEqual([[
- error.error("m.emb", attr.name.source_location,
- "Unknown attribute '(cpp) byte_order' on module 'm.emb'.")
+ error.error(
+ "m.emb", attr.value.source_location,
+ "Attribute 'expected_back_ends' must be a comma-delimited list of "
+ "back end specifiers (like \"cpp, proto\")), not \"c++\".")
]], attribute_checker.normalize_and_verify(ir))
+ def test_rejects_expected_back_ends_with_no_comma(self):
+ ir = _make_ir_from_emb('[expected_back_ends: "cpp z"]\n')
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error(
+ "m.emb", attr.value.source_location,
+ "Attribute 'expected_back_ends' must be a comma-delimited list of "
+ "back end specifiers (like \"cpp, proto\")), not \"cpp z\".")
+ ]], attribute_checker.normalize_and_verify(ir))
+
+ def test_rejects_expected_back_ends_with_extra_commas(self):
+ ir = _make_ir_from_emb('[expected_back_ends: "cpp,,z"]\n')
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error(
+ "m.emb", attr.value.source_location,
+ "Attribute 'expected_back_ends' must be a comma-delimited list of "
+ "back end specifiers (like \"cpp, proto\")), not \"cpp,,z\".")
+ ]], attribute_checker.normalize_and_verify(ir))
+
+ def test_accepts_empty_expected_back_ends(self):
+ ir = _make_ir_from_emb('[expected_back_ends: ""]\n')
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+
def test_adds_byte_order_attributes_from_default(self):
ir = _make_ir_from_emb('[$default byte_order: "BigEndian"]\n'
"struct Foo:\n"
diff --git a/compiler/front_end/attributes.py b/compiler/front_end/attributes.py
index f931f5b..e4561da 100644
--- a/compiler/front_end/attributes.py
+++ b/compiler/front_end/attributes.py
@@ -25,3 +25,4 @@
TEXT_OUTPUT = "text_output"
ENUM_MAXIMUM_BITS = "maximum_bits"
IS_SIGNED = "is_signed"
+BACK_ENDS = "expected_back_ends"
diff --git a/compiler/front_end/emboss_front_end.py b/compiler/front_end/emboss_front_end.py
index ed435c2..6388128 100644
--- a/compiler/front_end/emboss_front_end.py
+++ b/compiler/front_end/emboss_front_end.py
@@ -86,11 +86,11 @@
return parser.parse_args(argv[1:])
-def _show_errors(errors, debug_info, flags):
+def _show_errors(errors, ir, flags):
"""Prints errors with source code snippets."""
source_codes = {}
- for source_file in debug_info.modules:
- source_codes[source_file] = debug_info.modules[source_file].source_code
+ for module in ir.module:
+ source_codes[module.source_file_name] = module.source_text
use_color = (flags.color_output == "always" or
(flags.color_output in ("auto", "if_tty") and
os.isatty(sys.stderr.fileno())))
@@ -132,7 +132,7 @@
ir, debug_info, errors = glue.parse_emboss_file(
flags.input_file[0], _find_in_dirs_and_read(flags.import_dirs))
if errors:
- _show_errors(errors, debug_info, flags)
+ _show_errors(errors, flags)
return 1
main_module_debug_info = debug_info.modules[flags.input_file[0]]
if flags.debug_show_tokenization:
diff --git a/compiler/front_end/glue.py b/compiler/front_end/glue.py
index 261027d..c19385d 100644
--- a/compiler/front_end/glue.py
+++ b/compiler/front_end/glue.py
@@ -160,6 +160,7 @@
debug_info.parse_tree = parse_result.parse_tree
used_productions = set()
ir = module_ir.build_ir(parse_result.parse_tree, used_productions)
+ ir.source_text = source_code
debug_info.used_productions = used_productions
debug_info.ir = ir_pb2.Module()
debug_info.ir.CopyFrom(ir)
diff --git a/compiler/front_end/module_ir_test.py b/compiler/front_end/module_ir_test.py
index 0bbd46c..1700888 100644
--- a/compiler/front_end/module_ir_test.py
+++ b/compiler/front_end/module_ir_test.py
@@ -27,11 +27,10 @@
from compiler.util import ir_pb2
_TESTDATA_PATH = "testdata.golden"
+_MINIMAL_SOURCE = pkgutil.get_data(
+ _TESTDATA_PATH, "span_se_log_file_status.emb").decode(encoding="UTF-8")
_MINIMAL_SAMPLE = parser.parse_module(
- tokenizer.tokenize(
- pkgutil.get_data(_TESTDATA_PATH, "span_se_log_file_status.emb").decode(
- encoding="UTF-8"),
- "")[0]).parse_tree
+ tokenizer.tokenize(_MINIMAL_SOURCE, "")[0]).parse_tree
_MINIMAL_SAMPLE_IR = ir_pb2.Module.from_json(
pkgutil.get_data(_TESTDATA_PATH, "span_se_log_file_status.ir.txt").decode(
encoding="UTF-8")
@@ -4031,7 +4030,9 @@
"""Tests the module_ir.build_ir() function."""
def test_build_ir(self):
- self.assertEqual(module_ir.build_ir(_MINIMAL_SAMPLE), _MINIMAL_SAMPLE_IR)
+ ir = module_ir.build_ir(_MINIMAL_SAMPLE)
+ ir.source_text = _MINIMAL_SOURCE
+ self.assertEqual(ir, _MINIMAL_SAMPLE_IR)
def test_production_coverage(self):
"""Checks that all grammar productions are used somewhere in tests."""
diff --git a/compiler/util/BUILD b/compiler/util/BUILD
index 92f4cde..c9ec128 100644
--- a/compiler/util/BUILD
+++ b/compiler/util/BUILD
@@ -47,8 +47,19 @@
python_version = "PY3",
deps = [
":expression_parser",
- ":ir_util",
":ir_pb2",
+ ":ir_util",
+ ],
+)
+
+py_library(
+ name = "attribute_util",
+ srcs = ["attribute_util.py"],
+ deps = [
+ ":error",
+ ":ir_pb2",
+ ":ir_util",
+ ":traverse_ir",
],
)
diff --git a/compiler/util/attribute_util.py b/compiler/util/attribute_util.py
new file mode 100644
index 0000000..80387bc
--- /dev/null
+++ b/compiler/util/attribute_util.py
@@ -0,0 +1,275 @@
+# 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,
+ 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.
+ 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))
+
+ 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, 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
diff --git a/compiler/util/ir_pb2.py b/compiler/util/ir_pb2.py
index e175349..ef081bc 100644
--- a/compiler/util/ir_pb2.py
+++ b/compiler/util/ir_pb2.py
@@ -991,8 +991,9 @@
type = Repeated(TypeDefinition) # Module-level type definitions.
documentation = Repeated(Documentation) # Module-level docs.
foreign_import = Repeated(Import) # Other modules imported.
- source_location = Optional(Location) # Source code covered by this IR.
- source_file_name = Optional(_Text) # Name of the source file.
+ source_text = Optional(_Text) # The original source code.
+ source_location = Optional(Location) # Source code covered by this IR.
+ source_file_name = Optional(_Text) # Name of the source file.
@message
diff --git a/testdata/golden/span_se_log_file_status.ir.txt b/testdata/golden/span_se_log_file_status.ir.txt
index 942b49a..b1bfef8 100644
--- a/testdata/golden/span_se_log_file_status.ir.txt
+++ b/testdata/golden/span_se_log_file_status.ir.txt
@@ -1083,5 +1083,6 @@
"column": 1
},
"is_synthetic": false
- }
+ },
+ "source_text": "# Copyright 2019 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n-- This is a simple, real-world example structure.\n\n[$default byte_order: \"LittleEndian\"]\n[(cpp) namespace: \"emboss::test\"]\n\n\nstruct LogFileStatus:\n 0 [+4] UInt file_state\n 4 [+12] UInt:8[12] file_name\n 16 [+4] UInt file_size_kb\n 20 [+4] UInt media\n"
}