Implement `enum_case` attribute in cpp backend (#89)

* Implement `enum_case` attribute in cpp backend

Implements the `enum_case` attribute in the C++ backend to support
emitting enum values with a case other than SHOUTY_CASE. Currently only
the original SHOUTY_CASE and the new kCamelCase cases are supported, but
adding a new case should be trivial.

Additionally, the implementation was designed to make it simple for a
`name` attribute to be added for enum values (other IR nodes should be
unaffected, it would be neither simpler nor harder to implement after
this commit for other nodes).

Tests were added for case conversions, header generation, and a cc_test.
All tests pass both in `bazel test` and `python -m unittest`.

A small change was made to the front-end, as some attribute handling
functions were hoisted into `compiler/util` for reuse in the backend.

* Fixes from PR comments

- Uses the original SHOUTY_CASE emboss names as the canonical name for
  enum values rather than the backend-specific names.
- Tests (and fixes) the text stream functions.
- Adds additional test cases.
- Uses indicative verb forms for docstrings rather than imperative.
- Handles errors in enum_case strings much better.
  - Gives the source location of the specific issue.
  - Checks for empty/leading commas, allows trailing commas.
  - Checks for duplicate cases.

* Update docs for enum_case feature.

* Fix comment

* Test and fix empty/whitespace enum_case values with no comma
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..5ef58a5 100644
--- a/compiler/back_end/cpp/generated_code_templates
+++ b/compiler/back_end/cpp/generated_code_templates
@@ -840,12 +840,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..b45993d 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",
@@ -66,6 +69,13 @@
 # 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_module_namespace(module):
   """Returns the C++ namespace of the module, as a list of components.
@@ -1232,6 +1242,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 +1313,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 +1380,131 @@
   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_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 = []
+  # Note, this does not yet verify `namespace` attributes. Namespace
+  # verification is done in _get_module_namespace.
+  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 +1517,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..1025a74 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,221 @@
                     "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])
+
 
 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/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