Merge pull request #96 from reventlov/fix_iterator_bug
Fix array iterators when the array view is a temporary object.
diff --git a/compiler/back_end/cpp/BUILD b/compiler/back_end/cpp/BUILD
index c67b686..3d31ec2 100644
--- a/compiler/back_end/cpp/BUILD
+++ b/compiler/back_end/cpp/BUILD
@@ -35,12 +35,19 @@
)
py_library(
+ name = "attributes",
+ srcs = ["attributes.py"],
+ deps = []
+)
+
+py_library(
name = "header_generator",
srcs = ["header_generator.py"],
data = [
"generated_code_templates",
],
deps = [
+ ":attributes",
"//compiler/back_end/util:code_template",
"//compiler/util:attribute_util",
"//compiler/util:ir_pb2",
@@ -115,6 +122,17 @@
)
emboss_cc_test(
+ name = "enum_case_test",
+ srcs = [
+ "testcode/enum_case_test.cc",
+ ],
+ deps = [
+ "//testdata:enum_case_emboss",
+ "@com_google_googletest//:gtest_main",
+ ],
+)
+
+emboss_cc_test(
name = "explicit_sizes_test",
srcs = [
"testcode/explicit_sizes_test.cc",
diff --git a/compiler/back_end/cpp/attributes.py b/compiler/back_end/cpp/attributes.py
new file mode 100644
index 0000000..86efb72
--- /dev/null
+++ b/compiler/back_end/cpp/attributes.py
@@ -0,0 +1,57 @@
+# 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.
+
+"""Attributes in the C++ backend and associated metadata."""
+
+from enum import Enum
+from compiler.util import attribute_util
+
+
+class Attribute(str, Enum):
+ """Attributes available in the C++ backend."""
+ NAMESPACE = "namespace"
+ ENUM_CASE = "enum_case"
+
+
+# Types associated with C++ backend attributes.
+TYPES = {
+ Attribute.NAMESPACE: attribute_util.STRING,
+ Attribute.ENUM_CASE: attribute_util.STRING,
+}
+
+
+class Scope(set[tuple[Attribute, bool]], Enum):
+ """Allowed scopes for C++ backend attributes.
+
+ Each entry is a set of (Attribute, default?) tuples, the first value being
+ the attribute itself, the second value being a boolean value indicating
+ whether the attribute is allowed to be defaulted in that scope."""
+ BITS = {
+ # Bits may contain an enum definition.
+ (Attribute.ENUM_CASE, True)
+ }
+ ENUM = {
+ (Attribute.ENUM_CASE, True),
+ }
+ ENUM_VALUE = {
+ (Attribute.ENUM_CASE, False),
+ }
+ MODULE = {
+ (Attribute.NAMESPACE, False),
+ (Attribute.ENUM_CASE, True),
+ },
+ STRUCT = {
+ # Struct may contain an enum definition.
+ (Attribute.ENUM_CASE, True),
+ }
diff --git a/compiler/back_end/cpp/generated_code_templates b/compiler/back_end/cpp/generated_code_templates
index 4541bdb..4554b39 100644
--- a/compiler/back_end/cpp/generated_code_templates
+++ b/compiler/back_end/cpp/generated_code_templates
@@ -37,7 +37,9 @@
$_includes_$
+// NOLINTBEGIN
$_body_$
+// NOLINTEND
#endif // $_header_guard_$
@@ -840,12 +842,12 @@
// ** enum_from_name_case ** ///////////////////////////////////////////////////
if (!strcmp("$_name_$", emboss_reserved_local_name)) {
- *emboss_reserved_local_result = $_enum_$::$_name_$;
+ *emboss_reserved_local_result = $_enum_$::$_value_$;
return true;
}
// ** name_from_enum_case ** ///////////////////////////////////////////////////
- case $_enum_$::$_name_$: return "$_name_$";
+ case $_enum_$::$_value_$: return "$_name_$";
// ** enum_is_known_case ** ////////////////////////////////////////////////////
case $_enum_$::$_name_$: return true;
diff --git a/compiler/back_end/cpp/header_generator.py b/compiler/back_end/cpp/header_generator.py
index e467284..922d80b 100644
--- a/compiler/back_end/cpp/header_generator.py
+++ b/compiler/back_end/cpp/header_generator.py
@@ -22,11 +22,14 @@
import pkgutil
import re
+from compiler.back_end.cpp import attributes
from compiler.back_end.util import code_template
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 name_conversion
+from compiler.util import traverse_ir
_TEMPLATES = code_template.parse_templates(pkgutil.get_data(
"compiler.back_end.cpp",
@@ -63,9 +66,41 @@
# Emboss C++ support classes.
_SUPPORT_NAMESPACE = "::emboss::support"
+# Regex matching a C++ namespace component. Captures component name.
+_NS_COMPONENT_RE = r"(?:^\s*|::)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\s*$|::)"
+# Regex matching a full C++ namespace (at least one namespace component).
+_NS_RE = fr"^\s*(?:{_NS_COMPONENT_RE})+\s*$"
+# Regex matching an empty C++ namespace.
+_NS_EMPTY_RE = r"^\s*$"
+# Regex matching only the global C++ namespace.
+_NS_GLOBAL_RE = r"^\s*::\s*$"
+
# TODO(bolms): This should be a command-line flag.
_PRELUDE_INCLUDE_FILE = "runtime/cpp/emboss_prelude.h"
+# Cases allowed in the `enum_case` attribute.
+_SUPPORTED_ENUM_CASES = ("SHOUTY_CASE", "kCamelCase")
+
+# Verify that all supported enum cases have valid, implemented conversions.
+for _enum_case in _SUPPORTED_ENUM_CASES:
+ assert name_conversion.is_case_conversion_supported("SHOUTY_CASE", _enum_case)
+
+
+def _get_namespace_components(namespace):
+ """Gets the components of a C++ namespace
+
+ Examples:
+ "::some::name::detail" -> ["some", "name", "detail"]
+ "product::name" -> ["product", "name"]
+ "simple" -> ["simple"]
+
+ Arguments:
+ namespace: A string containing the namespace. May be fully-qualified.
+
+ Returns:
+ A list of strings, one per namespace component."""
+ return re.findall(_NS_COMPONENT_RE, namespace)
+
def _get_module_namespace(module):
"""Returns the C++ namespace of the module, as a list of components.
@@ -82,19 +117,7 @@
namespace = namespace_attr.string_constant.text
else:
namespace = "emboss_generated_code"
- if namespace[0:2] == "::":
- # If the user explicitly specified the leading "::", trim it off: it will be
- # re-added later, when the namespace is used as a prefix (as opposed to
- # "namespace foo { }").
- namespace = namespace[2:]
- namespace_list = namespace.split("::")
- for namespace_component in namespace_list:
- assert re.match("[a-zA-Z_][a-zA-Z0-9_]*", namespace_component), (
- "Bad namespace '{}'".format(namespace))
- assert namespace_component not in _CPP_RESERVED_WORDS, (
- "Reserved word '{}' is not allowed as a namespace component.".format(
- namespace_component))
- return namespace_list
+ return _get_namespace_components(namespace)
def _cpp_string_escape(string):
@@ -1232,6 +1255,65 @@
subtype_method_definitions + method_definitions)
+def _split_enum_case_values_into_spans(enum_case_value):
+ """Yields spans containing each enum case in an enum_case attribute value.
+
+ Each span is of the form (start, end), which is the start and end position
+ relative to the beginning of the enum_case_value string. To keep the grammar
+ of this attribute simple, this only splits on delimiters and trims whitespace
+ for each case.
+
+ Example: 'SHOUTY_CASE, kCamelCase' -> [(0, 11), (13, 23)]"""
+ # Scan the string from left to right, finding commas and trimming whitespace.
+ # This is essentially equivalent to (x.trim() fror x in str.split(','))
+ # except that this yields spans within the string rather than the strings
+ # themselves, and no span is yielded for a trailing comma.
+ start, end = 0, len(enum_case_value)
+ while start <= end:
+ # Find a ',' delimiter to split on
+ delimiter = enum_case_value.find(',', start, end)
+ if delimiter < 0:
+ delimiter = end
+
+ substr_start = start
+ substr_end = delimiter
+
+ # Drop leading whitespace
+ while (substr_start < substr_end and
+ enum_case_value[substr_start].isspace()):
+ substr_start += 1
+ # Drop trailing whitespace
+ while (substr_start < substr_end and
+ enum_case_value[substr_end - 1].isspace()):
+ substr_end -= 1
+
+ # Skip a trailing comma
+ if substr_start == end and start != 0:
+ break
+
+ yield substr_start, substr_end
+ start = delimiter + 1
+
+
+def _split_enum_case_values(enum_case_value):
+ """Returns all enum cases in an enum case value.
+
+ Example: 'SHOUTY_CASE, kCamelCase' -> ['SHOUTY_CASE', 'kCamelCase']"""
+ return [enum_case_value[start:end] for start, end
+ in _split_enum_case_values_into_spans(enum_case_value)]
+
+
+def _get_enum_value_names(enum_value):
+ """Determines one or more enum names based on attributes"""
+ cases = ["SHOUTY_CASE"]
+ name = enum_value.name.name.text
+ if enum_case := ir_util.get_attribute(enum_value.attribute,
+ attributes.Attribute.ENUM_CASE):
+ cases = _split_enum_case_values(enum_case.string_constant.text)
+ return [name_conversion.convert_case("SHOUTY_CASE", case, name)
+ for case in cases]
+
+
def _generate_enum_definition(type_ir):
"""Generates C++ for an Emboss enum."""
enum_values = []
@@ -1244,24 +1326,32 @@
enum_type = _cpp_integer_type_for_enum(max_bits, is_signed)
for value in type_ir.enumeration.value:
numeric_value = ir_util.constant_value(value.value)
- enum_values.append(
- code_template.format_template(_TEMPLATES.enum_value,
- name=value.name.name.text,
- value=_render_integer(numeric_value)))
- enum_from_string_statements.append(
- code_template.format_template(_TEMPLATES.enum_from_name_case,
- enum=type_ir.name.name.text,
- name=value.name.name.text))
- if numeric_value not in previously_seen_numeric_values:
- string_from_enum_statements.append(
- code_template.format_template(_TEMPLATES.name_from_enum_case,
+ enum_value_names = _get_enum_value_names(value)
+
+ for enum_value_name in enum_value_names:
+ enum_values.append(
+ code_template.format_template(_TEMPLATES.enum_value,
+ name=enum_value_name,
+ value=_render_integer(numeric_value)))
+
+ enum_from_string_statements.append(
+ code_template.format_template(_TEMPLATES.enum_from_name_case,
enum=type_ir.name.name.text,
+ value=enum_value_name,
name=value.name.name.text))
- enum_is_known_statements.append(
- code_template.format_template(_TEMPLATES.enum_is_known_case,
- enum=type_ir.name.name.text,
- name=value.name.name.text))
- previously_seen_numeric_values.add(numeric_value)
+
+ if numeric_value not in previously_seen_numeric_values:
+ string_from_enum_statements.append(
+ code_template.format_template(_TEMPLATES.name_from_enum_case,
+ enum=type_ir.name.name.text,
+ value=enum_value_name,
+ name=value.name.name.text))
+
+ enum_is_known_statements.append(
+ code_template.format_template(_TEMPLATES.enum_is_known_case,
+ enum=type_ir.name.name.text,
+ name=enum_value_name))
+ previously_seen_numeric_values.add(numeric_value)
return (
code_template.format_template(
_TEMPLATES.enum_declaration,
@@ -1303,6 +1393,162 @@
return no_double_underscore_path
+def _add_missing_enum_case_attribute_on_enum_value(enum_value, defaults):
+ """Adds an `enum_case` attribute if there isn't one but a default is set."""
+ if ir_util.get_attribute(enum_value.attribute,
+ attributes.Attribute.ENUM_CASE) is None:
+ if attributes.Attribute.ENUM_CASE in defaults:
+ enum_value.attribute.extend([defaults[attributes.Attribute.ENUM_CASE]])
+
+
+def _propagate_defaults(ir, targets, ancestors, add_fn):
+ """Propagates default values
+
+ Traverses the IR to propagate default values to target nodes.
+
+ Arguments:
+ targets: A list of target IR types to add attributes to.
+ ancestors: Ancestor types which may contain the default values.
+ add_fn: Function to add the attribute. May use any parameter available in
+ fast_traverse_ir_top_down actions as well as `defaults` containing the
+ default attributes set by ancestors.
+
+ Returns:
+ None
+ """
+ traverse_ir.fast_traverse_ir_top_down(
+ ir, targets, add_fn,
+ incidental_actions={
+ ancestor: attribute_util.gather_default_attributes
+ for ancestor in ancestors
+ },
+ parameters={"defaults": {}})
+
+
+def _offset_source_location_column(source_location, offset):
+ """Adds offsets from the start column of the supplied source location
+
+ Returns a new source location with all of the same properties as the provided
+ source location, but with the columns modified by offsets from the original
+ start column.
+
+ Offset should be a tuple of (start, end), which are the offsets relative to
+ source_location.start.column to set the new start.column and end.column."""
+
+ new_location = ir_pb2.Location()
+ new_location.CopyFrom(source_location)
+ new_location.start.column = source_location.start.column + offset[0]
+ new_location.end.column = source_location.start.column + offset[1]
+
+ return new_location
+
+
+def _verify_namespace_attribute(attr, source_file_name, errors):
+ if attr.name.text != attributes.Attribute.NAMESPACE:
+ return
+ namespace_value = attr.value.string_constant
+ if not re.match(_NS_RE, namespace_value.text):
+ if re.match(_NS_EMPTY_RE, namespace_value.text):
+ errors.append([error.error(
+ source_file_name, namespace_value.source_location,
+ 'Empty namespace value is not allowed.')])
+ elif re.match(_NS_GLOBAL_RE, namespace_value.text):
+ errors.append([error.error(
+ source_file_name, namespace_value.source_location,
+ 'Global namespace is not allowed.')])
+ else:
+ errors.append([error.error(
+ source_file_name, namespace_value.source_location,
+ 'Invalid namespace, must be a valid C++ namespace, such as "abc", '
+ '"abc::def", or "::abc::def::ghi" (ISO/IEC 14882:2017 '
+ 'enclosing-namespace-specifier).')])
+ return
+ for word in _get_namespace_components(namespace_value.text):
+ if word in _CPP_RESERVED_WORDS:
+ errors.append([error.error(
+ source_file_name, namespace_value.source_location,
+ f'Reserved word "{word}" is not allowed as a namespace component.'
+ )])
+
+
+def _verify_enum_case_attribute(attr, source_file_name, errors):
+ """Verify that `enum_case` values are supported."""
+ if attr.name.text != attributes.Attribute.ENUM_CASE:
+ return
+
+ VALID_CASES = ', '.join(case for case in _SUPPORTED_ENUM_CASES)
+ enum_case_value = attr.value.string_constant
+ case_spans = _split_enum_case_values_into_spans(enum_case_value.text)
+ seen_cases = set()
+
+ for start, end in case_spans:
+ case_source_location = _offset_source_location_column(
+ enum_case_value.source_location, (start, end))
+ case = enum_case_value.text[start:end]
+
+ if start == end:
+ errors.append([error.error(
+ source_file_name, case_source_location,
+ 'Empty enum case (or excess comma).')])
+ continue
+
+ if case in seen_cases:
+ errors.append([error.error(
+ source_file_name, case_source_location,
+ f'Duplicate enum case "{case}".')])
+ continue
+ seen_cases.add(case)
+
+ if case not in _SUPPORTED_ENUM_CASES:
+ errors.append([error.error(
+ source_file_name, case_source_location,
+ f'Unsupported enum case "{case}", '
+ f'supported cases are: {VALID_CASES}.')])
+
+
+def _verify_attribute_values(ir):
+ """Verify backend attribute values."""
+ errors = []
+
+ traverse_ir.fast_traverse_ir_top_down(
+ ir, [ir_pb2.Attribute], _verify_namespace_attribute,
+ parameters={"errors": errors})
+ traverse_ir.fast_traverse_ir_top_down(
+ ir, [ir_pb2.Attribute], _verify_enum_case_attribute,
+ parameters={"errors": errors})
+
+ return errors
+
+
+def _propagate_defaults_and_verify_attributes(ir):
+ """Verify attributes and ensure defaults are set when not overridden.
+
+ Returns a list of errors if there are errors present, or an empty list if
+ verification completed successfully."""
+ if errors := attribute_util.check_attributes_in_ir(
+ ir,
+ back_end="cpp",
+ types=attributes.TYPES,
+ module_attributes=attributes.Scope.MODULE,
+ struct_attributes=attributes.Scope.STRUCT,
+ bits_attributes=attributes.Scope.BITS,
+ enum_attributes=attributes.Scope.ENUM,
+ enum_value_attributes=attributes.Scope.ENUM_VALUE):
+ return errors
+
+ if errors := _verify_attribute_values(ir):
+ return errors
+
+ # Ensure defaults are set on EnumValues for `enum_case`.
+ _propagate_defaults(
+ ir,
+ targets=[ir_pb2.EnumValue],
+ ancestors=[ir_pb2.Module, ir_pb2.TypeDefinition],
+ add_fn=_add_missing_enum_case_attribute_on_enum_value)
+
+ return []
+
+
def generate_header(ir):
"""Generates a C++ header from an Emboss module.
@@ -1315,11 +1561,7 @@
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)})
+ errors = _propagate_defaults_and_verify_attributes(ir)
if errors:
return None, errors
type_declarations = []
diff --git a/compiler/back_end/cpp/header_generator_test.py b/compiler/back_end/cpp/header_generator_test.py
index f20c1ae..e67ed50 100644
--- a/compiler/back_end/cpp/header_generator_test.py
+++ b/compiler/back_end/cpp/header_generator_test.py
@@ -18,9 +18,9 @@
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 test_util
-
def _make_ir_from_emb(emb_text, name="m.emb"):
ir, unused_debug_info, errors = glue.parse_emboss_file(
name,
@@ -51,6 +51,325 @@
"Unknown attribute '(cpp) byte_order' on module 'm.emb'.")
]], header_generator.generate_header(ir)[1])
+ def test_accepts_enum_case(self):
+ mod_ir = _make_ir_from_emb('[(cpp) $default enum_case: "kCamelCase"]')
+ self.assertEqual([], header_generator.generate_header(mod_ir)[1])
+ enum_ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ self.assertEqual([], header_generator.generate_header(enum_ir)[1])
+ enum_value_ir = _make_ir_from_emb('enum Foo:\n'
+ ' BAR = 1 [(cpp) enum_case: "kCamelCase"]\n'
+ ' BAZ = 2\n'
+ ' [(cpp) enum_case: "kCamelCase"]\n')
+ self.assertEqual([], header_generator.generate_header(enum_value_ir)[1])
+ enum_in_struct_ir = _make_ir_from_emb('struct Outer:\n'
+ ' [(cpp) $default enum_case: "kCamelCase"]\n'
+ ' enum Inner:\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ self.assertEqual([], header_generator.generate_header(enum_in_struct_ir)[1])
+ enum_in_bits_ir = _make_ir_from_emb('bits Outer:\n'
+ ' [(cpp) $default enum_case: "kCamelCase"]\n'
+ ' enum Inner:\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ self.assertEqual([], header_generator.generate_header(enum_in_bits_ir)[1])
+ enum_ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE,"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ self.assertEqual([], header_generator.generate_header(enum_ir)[1])
+ enum_ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE ,kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ self.assertEqual([], header_generator.generate_header(enum_ir)[1])
+
+ def test_rejects_bad_enum_case_at_start(self):
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHORTY_CASE, kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ attr = ir.module[0].type[0].attribute[0]
+
+ bad_case_source_location = ir_pb2.Location()
+ bad_case_source_location.CopyFrom(attr.value.source_location)
+ # Location of SHORTY_CASE in the attribute line.
+ bad_case_source_location.start.column = 30
+ bad_case_source_location.end.column = 41
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Unsupported enum case "SHORTY_CASE", '
+ 'supported cases are: SHOUTY_CASE, kCamelCase.')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_bad_enum_case_in_middle(self):
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE, bad_CASE, kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ attr = ir.module[0].type[0].attribute[0]
+
+ bad_case_source_location = ir_pb2.Location()
+ bad_case_source_location.CopyFrom(attr.value.source_location)
+ # Location of bad_CASE in the attribute line.
+ bad_case_source_location.start.column = 43
+ bad_case_source_location.end.column = 51
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Unsupported enum case "bad_CASE", '
+ 'supported cases are: SHOUTY_CASE, kCamelCase.')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_bad_enum_case_at_end(self):
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase, BAD_case"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ attr = ir.module[0].type[0].attribute[0]
+
+ bad_case_source_location = ir_pb2.Location()
+ bad_case_source_location.CopyFrom(attr.value.source_location)
+ # Location of BAD_case in the attribute line.
+ bad_case_source_location.start.column = 55
+ bad_case_source_location.end.column = 63
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Unsupported enum case "BAD_case", '
+ 'supported cases are: SHOUTY_CASE, kCamelCase.')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_duplicate_enum_case(self):
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE, SHOUTY_CASE"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ attr = ir.module[0].type[0].attribute[0]
+
+ bad_case_source_location = ir_pb2.Location()
+ bad_case_source_location.CopyFrom(attr.value.source_location)
+ # Location of the second SHOUTY_CASE in the attribute line.
+ bad_case_source_location.start.column = 43
+ bad_case_source_location.end.column = 54
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Duplicate enum case "SHOUTY_CASE".')
+ ]], header_generator.generate_header(ir)[1])
+
+
+ def test_rejects_empty_enum_case(self):
+ # Double comma
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE,, kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+ attr = ir.module[0].type[0].attribute[0]
+
+ bad_case_source_location = ir_pb2.Location()
+ bad_case_source_location.CopyFrom(attr.value.source_location)
+ # Location of excess comma.
+ bad_case_source_location.start.column = 42
+ bad_case_source_location.end.column = 42
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ # Leading comma
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: ", SHOUTY_CASE, kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+
+ bad_case_source_location.start.column = 30
+ bad_case_source_location.end.column = 30
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ # Excess trailing comma
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase,,"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+
+ bad_case_source_location.start.column = 54
+ bad_case_source_location.end.column = 54
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ # Whitespace enum case
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: "SHOUTY_CASE, , kCamelCase"]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+
+ bad_case_source_location.start.column = 45
+ bad_case_source_location.end.column = 45
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ # Empty enum_case string
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: ""]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+
+ bad_case_source_location.start.column = 30
+ bad_case_source_location.end.column = 30
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ # Whitespace enum_case string
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: " "]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+
+ bad_case_source_location.start.column = 35
+ bad_case_source_location.end.column = 35
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ # One-character whitespace enum_case string
+ ir = _make_ir_from_emb('enum Foo:\n'
+ ' [(cpp) $default enum_case: " "]\n'
+ ' BAR = 1\n'
+ ' BAZ = 2\n')
+
+ bad_case_source_location.start.column = 31
+ bad_case_source_location.end.column = 31
+
+ self.assertEqual([[
+ error.error("m.emb", bad_case_source_location,
+ 'Empty enum case (or excess comma).')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_accepts_namespace(self):
+ for test in [
+ '[(cpp) namespace: "basic"]\n',
+ '[(cpp) namespace: "multiple::components"]\n',
+ '[(cpp) namespace: "::absolute"]\n',
+ '[(cpp) namespace: "::fully::qualified"]\n',
+ '[(cpp) namespace: "CAN::Be::cAPITAL"]\n',
+ '[(cpp) namespace: "trailingNumbers54321"]\n',
+ '[(cpp) namespace: "containing4321numbers"]\n',
+ '[(cpp) namespace: "can_have_underscores"]\n',
+ '[(cpp) namespace: "_initial_underscore"]\n',
+ '[(cpp) namespace: "_initial::_underscore"]\n',
+ '[(cpp) namespace: "::_initial::_underscore"]\n',
+ '[(cpp) namespace: "trailing_underscore_"]\n',
+ '[(cpp) namespace: "trailing_::underscore_"]\n',
+ '[(cpp) namespace: "::trailing_::underscore_"]\n',
+ '[(cpp) namespace: " spaces "]\n',
+ '[(cpp) namespace: "with :: spaces"]\n',
+ '[(cpp) namespace: " ::fully:: qualified :: with::spaces"]\n',
+ ]:
+ ir = _make_ir_from_emb(test)
+ self.assertEqual([], header_generator.generate_header(ir)[1])
+
+ def test_rejects_non_namespace_strings(self):
+ for test in [
+ '[(cpp) namespace: "5th::avenue"]\n',
+ '[(cpp) namespace: "can\'t::have::apostrophe"]\n',
+ '[(cpp) namespace: "cannot-have-dash"]\n',
+ '[(cpp) namespace: "no/slashes"]\n',
+ '[(cpp) namespace: "no\\\\slashes"]\n',
+ '[(cpp) namespace: "apostrophes*are*rejected"]\n',
+ '[(cpp) namespace: "avoid.dot"]\n',
+ '[(cpp) namespace: "does5+5"]\n',
+ '[(cpp) namespace: "=10"]\n',
+ '[(cpp) namespace: "?"]\n',
+ '[(cpp) namespace: "reject::spaces in::components"]\n',
+ '[(cpp) namespace: "totally::valid::but::extra +"]\n',
+ '[(cpp) namespace: "totally::valid::but::extra ::?"]\n',
+ '[(cpp) namespace: "< totally::valid::but::extra"]\n',
+ '[(cpp) namespace: "< ::totally::valid::but::extra"]\n',
+ '[(cpp) namespace: "::totally::valid::but::extra::"]\n',
+ '[(cpp) namespace: ":::extra::colon"]\n',
+ '[(cpp) namespace: "::extra:::colon"]\n',
+ ]:
+ ir = _make_ir_from_emb(test)
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error("m.emb", attr.value.source_location,
+ 'Invalid namespace, must be a valid C++ namespace, such '
+ 'as "abc", "abc::def", or "::abc::def::ghi" (ISO/IEC '
+ '14882:2017 enclosing-namespace-specifier).')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_empty_namespace(self):
+ for test in [
+ '[(cpp) namespace: ""]\n',
+ '[(cpp) namespace: " "]\n',
+ '[(cpp) namespace: " "]\n',
+ ]:
+ ir = _make_ir_from_emb(test)
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error("m.emb", attr.value.source_location,
+ 'Empty namespace value is not allowed.')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_global_namespace(self):
+ for test in [
+ '[(cpp) namespace: "::"]\n',
+ '[(cpp) namespace: " ::"]\n',
+ '[(cpp) namespace: ":: "]\n',
+ '[(cpp) namespace: " :: "]\n',
+ ]:
+ ir = _make_ir_from_emb(test)
+ attr = ir.module[0].attribute[0]
+ self.assertEqual([[
+ error.error("m.emb", attr.value.source_location,
+ 'Global namespace is not allowed.')
+ ]], header_generator.generate_header(ir)[1])
+
+ def test_rejects_reserved_namespace(self):
+ for test, expected in [
+ # Only component
+ ('[(cpp) namespace: "class"]\n', 'class'),
+ # Only component, fully qualified name
+ ('[(cpp) namespace: "::const"]\n', 'const'),
+ # First component
+ ('[(cpp) namespace: "if::valid"]\n', 'if'),
+ # First component, fully qualified name
+ ('[(cpp) namespace: "::auto::pilot"]\n', 'auto'),
+ # Last component
+ ('[(cpp) namespace: "make::do"]\n', 'do'),
+ # Middle component
+ ('[(cpp) namespace: "our::new::product"]\n', 'new'),
+ ]:
+ ir = _make_ir_from_emb(test)
+ attr = ir.module[0].attribute[0]
+
+ self.assertEqual([[
+ error.error("m.emb", attr.value.source_location,
+ f'Reserved word "{expected}" is not allowed '
+ f'as a namespace component.')]],
+ header_generator.generate_header(ir)[1])
+
if __name__ == "__main__":
unittest.main()
diff --git a/compiler/back_end/cpp/testcode/enum_case_test.cc b/compiler/back_end/cpp/testcode/enum_case_test.cc
new file mode 100644
index 0000000..247ad9d
--- /dev/null
+++ b/compiler/back_end/cpp/testcode/enum_case_test.cc
@@ -0,0 +1,303 @@
+// 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 the `enum_case` attribute generating the correct case. Note that since
+// these tests are regarding the name of enum members, it is likely that if this
+// test would fail, it may fail to compile.
+
+#include "gtest/gtest.h"
+#include "testdata/enum_case.emb.h"
+
+namespace emboss {
+namespace test {
+namespace {
+
+TEST(EnumShouty, AccessValuesByNameInSource) {
+ EXPECT_EQ(static_cast<int>(EnumShouty::FIRST), 0);
+ EXPECT_EQ(static_cast<int>(EnumShouty::SECOND), 1);
+ EXPECT_EQ(static_cast<int>(EnumShouty::TWO_WORD), 2);
+ EXPECT_EQ(static_cast<int>(EnumShouty::THREE_WORD_ENUM), 4);
+ EXPECT_EQ(static_cast<int>(EnumShouty::LONG_ENUM_VALUE_NAME), 8);
+}
+
+TEST(EnumShouty, EnumIsKnown) {
+ EXPECT_TRUE(EnumIsKnown(EnumShouty::FIRST));
+ EXPECT_TRUE(EnumIsKnown(EnumShouty::SECOND));
+ EXPECT_TRUE(EnumIsKnown(EnumShouty::TWO_WORD));
+ EXPECT_TRUE(EnumIsKnown(EnumShouty::THREE_WORD_ENUM));
+ EXPECT_TRUE(EnumIsKnown(EnumShouty::LONG_ENUM_VALUE_NAME));
+ EXPECT_FALSE(EnumIsKnown(static_cast<EnumShouty>(999)));
+}
+
+TEST(EnumShouty, NameToEnum) {
+ EnumShouty result;
+
+ EXPECT_TRUE(TryToGetEnumFromName("FIRST", &result));
+ EXPECT_EQ(EnumShouty::FIRST, result);
+ EXPECT_TRUE(TryToGetEnumFromName("SECOND", &result));
+ EXPECT_EQ(EnumShouty::SECOND, result);
+ EXPECT_TRUE(TryToGetEnumFromName("TWO_WORD", &result));
+ EXPECT_EQ(EnumShouty::TWO_WORD, result);
+ EXPECT_TRUE(TryToGetEnumFromName("THREE_WORD_ENUM", &result));
+ EXPECT_EQ(EnumShouty::THREE_WORD_ENUM, result);
+ EXPECT_TRUE(TryToGetEnumFromName("LONG_ENUM_VALUE_NAME", &result));
+ EXPECT_EQ(EnumShouty::LONG_ENUM_VALUE_NAME, result);
+}
+
+TEST(EnumShouty, NameToEnumFailsWithKCamel) {
+ EnumShouty result = EnumShouty::FIRST;
+
+ EXPECT_FALSE(TryToGetEnumFromName("kSecond", &result));
+ EXPECT_EQ(EnumShouty::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kTwoWord", &result));
+ EXPECT_EQ(EnumShouty::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kThreeWordEnum", &result));
+ EXPECT_EQ(EnumShouty::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kLongEnumValueName", &result));
+ EXPECT_EQ(EnumShouty::FIRST, result);
+}
+
+TEST(EnumShouty, EnumToName) {
+ EXPECT_EQ("FIRST", TryToGetNameFromEnum(EnumShouty::FIRST));
+ EXPECT_EQ("SECOND", TryToGetNameFromEnum(EnumShouty::SECOND));
+ EXPECT_EQ("TWO_WORD", TryToGetNameFromEnum(EnumShouty::TWO_WORD));
+ EXPECT_EQ("THREE_WORD_ENUM",
+ TryToGetNameFromEnum(EnumShouty::THREE_WORD_ENUM));
+ EXPECT_EQ("LONG_ENUM_VALUE_NAME",
+ TryToGetNameFromEnum(EnumShouty::LONG_ENUM_VALUE_NAME));
+}
+
+TEST(EnumDefault, AccessValuesByNameInSource) {
+ EXPECT_EQ(static_cast<int>(EnumDefault::kFirst), 0);
+ EXPECT_EQ(static_cast<int>(EnumDefault::kSecond), 1);
+ EXPECT_EQ(static_cast<int>(EnumDefault::kTwoWord), 2);
+ EXPECT_EQ(static_cast<int>(EnumDefault::kThreeWordEnum), 4);
+ EXPECT_EQ(static_cast<int>(EnumDefault::kLongEnumValueName), 8);
+}
+
+TEST(EnumDefault, EnumIsKnown) {
+ EXPECT_TRUE(EnumIsKnown(EnumDefault::kFirst));
+ EXPECT_TRUE(EnumIsKnown(EnumDefault::kSecond));
+ EXPECT_TRUE(EnumIsKnown(EnumDefault::kTwoWord));
+ EXPECT_TRUE(EnumIsKnown(EnumDefault::kThreeWordEnum));
+ EXPECT_TRUE(EnumIsKnown(EnumDefault::kLongEnumValueName));
+ EXPECT_FALSE(EnumIsKnown(static_cast<EnumDefault>(999)));
+}
+
+TEST(EnumDefault, NameToEnum) {
+ EnumDefault result;
+
+ EXPECT_TRUE(TryToGetEnumFromName("FIRST", &result));
+ EXPECT_EQ(EnumDefault::kFirst, result);
+ EXPECT_TRUE(TryToGetEnumFromName("SECOND", &result));
+ EXPECT_EQ(EnumDefault::kSecond, result);
+ EXPECT_TRUE(TryToGetEnumFromName("TWO_WORD", &result));
+ EXPECT_EQ(EnumDefault::kTwoWord, result);
+ EXPECT_TRUE(TryToGetEnumFromName("THREE_WORD_ENUM", &result));
+ EXPECT_EQ(EnumDefault::kThreeWordEnum, result);
+ EXPECT_TRUE(TryToGetEnumFromName("LONG_ENUM_VALUE_NAME", &result));
+ EXPECT_EQ(EnumDefault::kLongEnumValueName, result);
+}
+
+TEST(EnumDefault, NameToEnumFailsWithKCamel) {
+ EnumDefault result = EnumDefault::kFirst;
+
+ EXPECT_FALSE(TryToGetEnumFromName("kFirst", &result));
+ EXPECT_EQ(EnumDefault::kFirst, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kSecond", &result));
+ EXPECT_EQ(EnumDefault::kFirst, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kTwoWord", &result));
+ EXPECT_EQ(EnumDefault::kFirst, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kThreeWordEnum", &result));
+ EXPECT_EQ(EnumDefault::kFirst, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kLongEnumValueName", &result));
+ EXPECT_EQ(EnumDefault::kFirst, result);
+}
+
+TEST(EnumDefault, EnumToName) {
+ EXPECT_EQ("FIRST", TryToGetNameFromEnum(EnumDefault::kFirst));
+ EXPECT_EQ("SECOND", TryToGetNameFromEnum(EnumDefault::kSecond));
+ EXPECT_EQ("TWO_WORD", TryToGetNameFromEnum(EnumDefault::kTwoWord));
+ EXPECT_EQ("THREE_WORD_ENUM",
+ TryToGetNameFromEnum(EnumDefault::kThreeWordEnum));
+ EXPECT_EQ("LONG_ENUM_VALUE_NAME",
+ TryToGetNameFromEnum(EnumDefault::kLongEnumValueName));
+}
+
+TEST(EnumShoutyAndKCamel, AccessValuesByNameInSource) {
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::FIRST), 0);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::kFirst), 0);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::SECOND), 1);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::kSecond), 1);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::TWO_WORD), 2);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::kTwoWord), 2);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::THREE_WORD_ENUM), 4);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::kThreeWordEnum), 4);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::LONG_ENUM_VALUE_NAME), 8);
+ EXPECT_EQ(static_cast<int>(EnumShoutyAndKCamel::kLongEnumValueName), 8);
+}
+
+TEST(EnumShoutyAndKCamel, EnumIsKnown) {
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::FIRST));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::SECOND));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::TWO_WORD));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::THREE_WORD_ENUM));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::LONG_ENUM_VALUE_NAME));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::kFirst));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::kSecond));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::kTwoWord));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::kThreeWordEnum));
+ EXPECT_TRUE(EnumIsKnown(EnumShoutyAndKCamel::kLongEnumValueName));
+ EXPECT_FALSE(EnumIsKnown(static_cast<EnumShoutyAndKCamel>(999)));
+}
+
+TEST(EnumShoutyAndKCamel, NameToEnum) {
+ EnumShoutyAndKCamel result;
+
+ EXPECT_TRUE(TryToGetEnumFromName("FIRST", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_TRUE(TryToGetEnumFromName("SECOND", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::SECOND, result);
+ EXPECT_TRUE(TryToGetEnumFromName("TWO_WORD", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::TWO_WORD, result);
+ EXPECT_TRUE(TryToGetEnumFromName("THREE_WORD_ENUM", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::THREE_WORD_ENUM, result);
+ EXPECT_TRUE(TryToGetEnumFromName("LONG_ENUM_VALUE_NAME", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::LONG_ENUM_VALUE_NAME, result);
+}
+
+TEST(EnumShoutyAndKCamel, NameToEnumFailsWithKCamel) {
+ EnumShoutyAndKCamel result = EnumShoutyAndKCamel::FIRST;
+
+ EXPECT_FALSE(TryToGetEnumFromName("kFirst", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kSecond", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kTwoWord", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kThreeWordEnum", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kLongEnumValueName", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+}
+
+TEST(EnumShoutyAndKCamel, EnumToName) {
+ EXPECT_EQ("FIRST", TryToGetNameFromEnum(EnumShoutyAndKCamel::FIRST));
+ EXPECT_EQ("SECOND", TryToGetNameFromEnum(EnumShoutyAndKCamel::SECOND));
+ EXPECT_EQ("TWO_WORD", TryToGetNameFromEnum(EnumShoutyAndKCamel::TWO_WORD));
+ EXPECT_EQ("THREE_WORD_ENUM",
+ TryToGetNameFromEnum(EnumShoutyAndKCamel::THREE_WORD_ENUM));
+ EXPECT_EQ("LONG_ENUM_VALUE_NAME",
+ TryToGetNameFromEnum(EnumShoutyAndKCamel::LONG_ENUM_VALUE_NAME));
+
+ EXPECT_EQ("FIRST", TryToGetNameFromEnum(EnumShoutyAndKCamel::kFirst));
+ EXPECT_EQ("SECOND", TryToGetNameFromEnum(EnumShoutyAndKCamel::kSecond));
+ EXPECT_EQ("TWO_WORD", TryToGetNameFromEnum(EnumShoutyAndKCamel::kTwoWord));
+ EXPECT_EQ("THREE_WORD_ENUM",
+ TryToGetNameFromEnum(EnumShoutyAndKCamel::kThreeWordEnum));
+ EXPECT_EQ("LONG_ENUM_VALUE_NAME",
+ TryToGetNameFromEnum(EnumShoutyAndKCamel::kLongEnumValueName));
+}
+
+TEST(EnumMixed, AccessValuesByNameInSource) {
+ EXPECT_EQ(static_cast<int>(EnumMixed::FIRST), 0);
+ EXPECT_EQ(static_cast<int>(EnumMixed::kFirst), 0);
+ EXPECT_EQ(static_cast<int>(EnumMixed::SECOND), 1);
+ EXPECT_EQ(static_cast<int>(EnumMixed::kTwoWord), 2);
+ EXPECT_EQ(static_cast<int>(EnumMixed::THREE_WORD_ENUM), 4);
+ EXPECT_EQ(static_cast<int>(EnumMixed::kThreeWordEnum), 4);
+ EXPECT_EQ(static_cast<int>(EnumMixed::kLongEnumValueName), 8);
+}
+
+TEST(EnumMixed, EnumIsKnown) {
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::FIRST));
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::SECOND));
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::THREE_WORD_ENUM));
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::kFirst));
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::kTwoWord));
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::kThreeWordEnum));
+ EXPECT_TRUE(EnumIsKnown(EnumMixed::kLongEnumValueName));
+ EXPECT_FALSE(EnumIsKnown(static_cast<EnumMixed>(999)));
+}
+
+TEST(EnumMixed, NameToEnum) {
+ EnumMixed result;
+
+ EXPECT_TRUE(TryToGetEnumFromName("FIRST", &result));
+ EXPECT_EQ(EnumMixed::FIRST, result);
+ EXPECT_TRUE(TryToGetEnumFromName("SECOND", &result));
+ EXPECT_EQ(EnumMixed::SECOND, result);
+ EXPECT_TRUE(TryToGetEnumFromName("TWO_WORD", &result));
+ EXPECT_EQ(EnumMixed::kTwoWord, result);
+ EXPECT_TRUE(TryToGetEnumFromName("THREE_WORD_ENUM", &result));
+ EXPECT_EQ(EnumMixed::THREE_WORD_ENUM, result);
+ EXPECT_TRUE(TryToGetEnumFromName("LONG_ENUM_VALUE_NAME", &result));
+ EXPECT_EQ(EnumMixed::kLongEnumValueName, result);
+}
+
+TEST(EnumMixed, NameToEnumFailsWithKCamel) {
+ EnumShoutyAndKCamel result = EnumShoutyAndKCamel::FIRST;
+
+ EXPECT_FALSE(TryToGetEnumFromName("kFirst", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kSecond", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kTwoWord", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kThreeWordEnum", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+ EXPECT_FALSE(TryToGetEnumFromName("kLongEnumValueName", &result));
+ EXPECT_EQ(EnumShoutyAndKCamel::FIRST, result);
+}
+
+TEST(EnumMixed, EnumToName) {
+ EXPECT_EQ("FIRST", TryToGetNameFromEnum(EnumMixed::FIRST));
+ EXPECT_EQ("FIRST", TryToGetNameFromEnum(EnumMixed::kFirst));
+ EXPECT_EQ("SECOND", TryToGetNameFromEnum(EnumMixed::SECOND));
+ EXPECT_EQ("TWO_WORD", TryToGetNameFromEnum(EnumMixed::kTwoWord));
+ EXPECT_EQ("THREE_WORD_ENUM",
+ TryToGetNameFromEnum(EnumMixed::THREE_WORD_ENUM));
+ EXPECT_EQ("THREE_WORD_ENUM",
+ TryToGetNameFromEnum(EnumMixed::kThreeWordEnum));
+ EXPECT_EQ("LONG_ENUM_VALUE_NAME",
+ TryToGetNameFromEnum(EnumMixed::kLongEnumValueName));
+}
+
+TEST(UseKCamelEnumCase, IsValidToUse) {
+ std::array<uint8_t, UseKCamelEnumCase::IntrinsicSizeInBytes()> buffer;
+ auto view = MakeUseKCamelEnumCaseView(&buffer);
+
+ EXPECT_EQ(UseKCamelEnumCase::first(), EnumDefault::kFirst);
+ EXPECT_EQ(view.first().Read(), EnumDefault::kFirst);
+
+ EXPECT_TRUE(view.v().TryToWrite(EnumDefault::kSecond));
+ EXPECT_FALSE(view.v_is_first().Read());
+
+ EXPECT_TRUE(view.v().TryToWrite(EnumDefault::kFirst));
+ EXPECT_TRUE(view.v_is_first().Read());
+}
+
+TEST(UseKCamelEnumCase, TextStream) {
+ std::array<uint8_t, UseKCamelEnumCase::IntrinsicSizeInBytes()> buffer;
+ auto view = MakeUseKCamelEnumCaseView(&buffer);
+
+ EXPECT_TRUE(view.v().TryToWrite(EnumDefault::kSecond));
+ EXPECT_EQ(WriteToString(view), "{ v: SECOND }");
+ EXPECT_TRUE(UpdateFromText(view, "{ v: TWO_WORD }"));
+ EXPECT_EQ(view.v().Read(), EnumDefault::kTwoWord);
+}
+
+} // namespace
+} // namespace test
+} // namespace emboss
diff --git a/compiler/front_end/attribute_checker.py b/compiler/front_end/attribute_checker.py
index 21c6b22..cb8db74 100644
--- a/compiler/front_end/attribute_checker.py
+++ b/compiler/front_end/attribute_checker.py
@@ -397,17 +397,6 @@
])
-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(
@@ -419,17 +408,17 @@
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,
+ ir_pb2.Module: attribute_util.gather_default_attributes,
+ ir_pb2.TypeDefinition: attribute_util.gather_default_attributes,
+ ir_pb2.Field: attribute_util.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,
+ ir_pb2.Module: attribute_util.gather_default_attributes,
+ ir_pb2.TypeDefinition: attribute_util.gather_default_attributes,
+ ir_pb2.Field: attribute_util.gather_default_attributes,
},
parameters={"defaults": {}})
return []
diff --git a/compiler/front_end/emboss_front_end.py b/compiler/front_end/emboss_front_end.py
index 6388128..7df6782 100644
--- a/compiler/front_end/emboss_front_end.py
+++ b/compiler/front_end/emboss_front_end.py
@@ -89,8 +89,9 @@
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
+ if ir:
+ 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 +133,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, flags)
+ _show_errors(errors, ir, flags)
return 1
main_module_debug_info = debug_info.modules[flags.input_file[0]]
if flags.debug_show_tokenization:
diff --git a/compiler/util/attribute_util.py b/compiler/util/attribute_util.py
index 8d75de6..d864364 100644
--- a/compiler/util/attribute_util.py
+++ b/compiler/util/attribute_util.py
@@ -284,3 +284,26 @@
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}
diff --git a/compiler/util/name_conversion.py b/compiler/util/name_conversion.py
index 71aeff5..3f32809 100644
--- a/compiler/util/name_conversion.py
+++ b/compiler/util/name_conversion.py
@@ -14,6 +14,58 @@
"""Conversions between snake-, camel-, and shouty-case names."""
+from enum import Enum
+
+class Case(str, Enum):
+ SNAKE = "snake_case"
+ SHOUTY = "SHOUTY_CASE"
+ CAMEL = "CamelCase"
+ K_CAMEL = "kCamelCase"
+
+
+# Map of (from, to) cases to their conversion function. Initially only contains
+# identity case conversions, additional conversions are added with the
+# _case_conversion decorator.
+_case_conversions = {(case.value, case.value): lambda x: x for case in Case}
+
+
+def _case_conversion(case_from, case_to):
+ """Decorator to dynamically dispatch case conversions at runtime."""
+ def _func(f):
+ _case_conversions[case_from, case_to] = f
+ return f
+
+ return _func
+
+
+@_case_conversion(Case.SNAKE, Case.CAMEL)
+@_case_conversion(Case.SHOUTY, Case.CAMEL)
def snake_to_camel(name):
+ """Convert from snake_case to CamelCase. Also works from SHOUTY_CASE."""
return "".join(word.capitalize() for word in name.split("_"))
+
+
+@_case_conversion(Case.CAMEL, Case.K_CAMEL)
+def camel_to_k_camel(name):
+ """Convert from CamelCase to kCamelCase."""
+ return "k" + name
+
+
+@_case_conversion(Case.SNAKE, Case.K_CAMEL)
+@_case_conversion(Case.SHOUTY, Case.K_CAMEL)
+def snake_to_k_camel(name):
+ """Converts from snake_case to kCamelCase. Also works from SHOUTY_CASE."""
+ return camel_to_k_camel(snake_to_camel(name))
+
+
+def convert_case(case_from, case_to, value):
+ """Converts cases based on runtime case values.
+
+ Note: Cases can be strings or enum values."""
+ return _case_conversions[case_from, case_to](value)
+
+
+def is_case_conversion_supported(case_from, case_to):
+ """Determines if a case conversion would be supported"""
+ return (case_from, case_to) in _case_conversions
diff --git a/compiler/util/name_conversion_test.py b/compiler/util/name_conversion_test.py
index a980a1d..9e41ecc 100644
--- a/compiler/util/name_conversion_test.py
+++ b/compiler/util/name_conversion_test.py
@@ -29,6 +29,61 @@
self.assertEqual("Abc89Def", name_conversion.snake_to_camel("abc_89_def"))
self.assertEqual("Abc89def", name_conversion.snake_to_camel("abc_89def"))
+ def test_shouty_to_camel(self):
+ self.assertEqual("Abc", name_conversion.snake_to_camel("ABC"))
+ self.assertEqual("AbcDef", name_conversion.snake_to_camel("ABC_DEF"))
+ self.assertEqual("AbcDef89", name_conversion.snake_to_camel("ABC_DEF89"))
+ self.assertEqual("AbcDef89", name_conversion.snake_to_camel("ABC_DEF_89"))
+ self.assertEqual("Abc89Def", name_conversion.snake_to_camel("ABC_89_DEF"))
+ self.assertEqual("Abc89def", name_conversion.snake_to_camel("ABC_89DEF"))
+
+ def test_camel_to_k_camel(self):
+ self.assertEqual("kFoo", name_conversion.camel_to_k_camel("Foo"))
+ self.assertEqual("kFooBar", name_conversion.camel_to_k_camel("FooBar"))
+ self.assertEqual("kAbc123", name_conversion.camel_to_k_camel("Abc123"))
+
+ def test_snake_to_k_camel(self):
+ self.assertEqual("kAbc", name_conversion.snake_to_k_camel("abc"))
+ self.assertEqual("kAbcDef", name_conversion.snake_to_k_camel("abc_def"))
+ self.assertEqual("kAbcDef89",
+ name_conversion.snake_to_k_camel("abc_def89"))
+ self.assertEqual("kAbcDef89",
+ name_conversion.snake_to_k_camel("abc_def_89"))
+ self.assertEqual("kAbc89Def",
+ name_conversion.snake_to_k_camel("abc_89_def"))
+ self.assertEqual("kAbc89def",
+ name_conversion.snake_to_k_camel("abc_89def"))
+
+ def test_shouty_to_k_camel(self):
+ self.assertEqual("kAbc", name_conversion.snake_to_k_camel("ABC"))
+ self.assertEqual("kAbcDef", name_conversion.snake_to_k_camel("ABC_DEF"))
+ self.assertEqual("kAbcDef89",
+ name_conversion.snake_to_k_camel("ABC_DEF89"))
+ self.assertEqual("kAbcDef89",
+ name_conversion.snake_to_k_camel("ABC_DEF_89"))
+ self.assertEqual("kAbc89Def",
+ name_conversion.snake_to_k_camel("ABC_89_DEF"))
+ self.assertEqual("kAbc89def",
+ name_conversion.snake_to_k_camel("ABC_89DEF"))
+
+ def test_convert_case(self):
+ self.assertEqual("foo_bar_123", name_conversion.convert_case(
+ "snake_case", "snake_case", "foo_bar_123"))
+ self.assertEqual("FOO_BAR_123", name_conversion.convert_case(
+ "SHOUTY_CASE", "SHOUTY_CASE", "FOO_BAR_123"))
+ self.assertEqual("kFooBar123", name_conversion.convert_case(
+ "kCamelCase", "kCamelCase", "kFooBar123"))
+ self.assertEqual("FooBar123", name_conversion.convert_case(
+ "CamelCase", "CamelCase", "FooBar123"))
+ self.assertEqual("kAbcDef", name_conversion.convert_case(
+ "snake_case", "kCamelCase", "abc_def"))
+ self.assertEqual("AbcDef", name_conversion.convert_case(
+ "snake_case", "CamelCase", "abc_def"))
+ self.assertEqual("kAbcDef", name_conversion.convert_case(
+ "SHOUTY_CASE", "kCamelCase", "ABC_DEF"))
+ self.assertEqual("AbcDef", name_conversion.convert_case(
+ "SHOUTY_CASE", "CamelCase", "ABC_DEF"))
+
if __name__ == "__main__":
unittest.main()
diff --git a/doc/design_docs/alternate_enum_cases.md b/doc/design_docs/archive/alternate_enum_cases.md
similarity index 98%
rename from doc/design_docs/alternate_enum_cases.md
rename to doc/design_docs/archive/alternate_enum_cases.md
index 846db26..eb2c76c 100644
--- a/doc/design_docs/alternate_enum_cases.md
+++ b/doc/design_docs/archive/alternate_enum_cases.md
@@ -1,5 +1,9 @@
# Design: Alternate Enum Field Cases
+This document is provided for historical interest. This feature is now
+implemented in the form of the `[enum_case]` attribute on `enum` values, which
+can also be `$default`ed on module, struct, bits, and enum definitions.
+
## Motivation
Currently, the Emboss compiler requires that enum fields are `SHOUTY_CASE`, but
diff --git a/doc/language-reference.md b/doc/language-reference.md
index 7eda923..40da052 100644
--- a/doc/language-reference.md
+++ b/doc/language-reference.md
@@ -222,6 +222,33 @@
The `namespace` attribute may only be used at the module level; all structures
and enums within a module will be placed in the same namespace.
+### `(cpp) enum_case`
+
+The `enum_case` attribute can be specified for the C++ backend to specify
+in which case the enum values should be emitted to generated source. It does
+not change the text representation, which always uses the original emboss
+definition name as the canonical name.
+
+Currently, the supported cases are`SHOUTY_CASE` and `kCamelCase`.
+
+A `$default` enum case can be set on a module, struct, bits, or enum and
+applies to all enum values within that module, struct, bits, or enum
+definition.
+
+For example, to use `kCamelCase` by default for all enum values in a module:
+
+```
+[$default enum_case: "kCamelCase"]
+```
+
+This will change enum names like `UPPER_CHANNEL_RANGE_LIMIT` to
+`kUpperChannelRangeLimit` in the C++ source for all enum values in the module.
+Multiple case names can be specified, which is especially useful when
+transitioning between two cases:
+
+```
+[enum_case: "SHOUTY_CASE, kCamelCase"]
+```
### `text_output`
diff --git a/testdata/BUILD b/testdata/BUILD
index 083298c..8ee7327 100644
--- a/testdata/BUILD
+++ b/testdata/BUILD
@@ -47,6 +47,7 @@
"cpp_namespace.emb",
"dynamic_size.emb",
"enum.emb",
+ "enum_case.emb",
"explicit_sizes.emb",
"float.emb",
"imported.emb",
@@ -107,6 +108,13 @@
)
emboss_cc_library(
+ name = "enum_case_emboss",
+ srcs = [
+ "enum_case.emb",
+ ],
+)
+
+emboss_cc_library(
name = "explicit_sizes_emboss",
srcs = [
"explicit_sizes.emb",
diff --git a/testdata/enum_case.emb b/testdata/enum_case.emb
new file mode 100644
index 0000000..04f3bfe
--- /dev/null
+++ b/testdata/enum_case.emb
@@ -0,0 +1,55 @@
+# 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.
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "emboss::test"]
+[(cpp) $default enum_case: "kCamelCase"]
+
+enum EnumShouty:
+ [(cpp) $default enum_case: "SHOUTY_CASE"]
+ FIRST = 0
+ SECOND = 1
+ TWO_WORD = 2
+ THREE_WORD_ENUM = 4
+ LONG_ENUM_VALUE_NAME = 8
+
+enum EnumDefault:
+ FIRST = 0
+ SECOND = 1
+ TWO_WORD = 2
+ THREE_WORD_ENUM = 4
+ LONG_ENUM_VALUE_NAME = 8
+
+struct UseKCamelEnumCase:
+ 0 [+4] EnumDefault v
+ let first = EnumDefault.FIRST
+ let v_is_first = v == EnumDefault.FIRST
+
+enum EnumShoutyAndKCamel:
+ [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase"]
+ FIRST = 0
+ SECOND = 1
+ TWO_WORD = 2
+ THREE_WORD_ENUM = 4
+ LONG_ENUM_VALUE_NAME = 8
+
+enum EnumMixed:
+ -- Tests mixing various `enum_case` values in the same enum definition.
+ FIRST = 0 [(cpp) enum_case: "SHOUTY_CASE, kCamelCase"]
+ SECOND = 1 [(cpp) enum_case: "SHOUTY_CASE"]
+ TWO_WORD = 2
+ [(cpp) enum_case: "kCamelCase"]
+ THREE_WORD_ENUM = 4
+ [(cpp) enum_case: "kCamelCase, SHOUTY_CASE"]
+ LONG_ENUM_VALUE_NAME = 8