Add attributes to enum values (#86)

* Add attributes to enum values

As of this change, the grammar allows attributes on enum values, either
inline or indented in the body, the same as fields. Additionally, this
change updates the formatter and grammar documentation to account for
this change.

* Check attributes of enum values

This ensures that enum values are checked during attribute verification.
Since currently there are no non-backend attributes for enum values,
having any (non-backend) attribute at all will produce an error.
diff --git a/compiler/front_end/attribute_checker_test.py b/compiler/front_end/attribute_checker_test.py
index def86c6..bb57ac9 100644
--- a/compiler/front_end/attribute_checker_test.py
+++ b/compiler/front_end/attribute_checker_test.py
@@ -680,6 +680,18 @@
             "'maximum_bits' on an 'enum' must be between 1 and 64.")]],
         error.filter_errors(attribute_checker.normalize_and_verify(ir)))
 
+  def test_rejects_unknown_enum_value_attribute(self):
+      ir = _make_ir_from_emb("enum Foo:\n"
+                             "  BAR = 0  \n"
+                             "    [bad_attr: true]\n")
+      attribute_ir = ir.module[0].type[0].enumeration.value[0].attribute[0]
+      self.assertNotEqual([], attribute_checker.normalize_and_verify(ir))
+      self.assertEqual(
+          [[error.error(
+              "m.emb", attribute_ir.name.source_location,
+              "Unknown attribute 'bad_attr' on enum value 'BAR'.")]],
+          error.filter_errors(attribute_checker.normalize_and_verify(ir)))
+
 
 if __name__ == "__main__":
   unittest.main()
diff --git a/compiler/front_end/format_emb.py b/compiler/front_end/format_emb.py
index ce5d16b..e4b5c23 100644
--- a/compiler/front_end/format_emb.py
+++ b/compiler/front_end/format_emb.py
@@ -606,17 +606,17 @@
   return value + block
 
 
-@_formats('enum-value -> constant-name "=" expression doc? Comment? eol'
+@_formats('enum-value -> constant-name "=" expression attribute* doc? Comment? eol'
           '              enum-value-body?')
-def _enum_value(name, equals, value, docs, comment, eol, body):
-  return [_Block([], _Row('enum-value', [name, equals, value, docs, comment]),
+def _enum_value(name, equals, value, attributes, docs, comment, eol, body):
+  return [_Block([], _Row('enum-value', [name, equals, value, attributes, docs, comment]),
                  eol + body)]
 
 
-@_formats('enum-value-body -> Indent doc-line* Dedent')
-def _enum_value_body(indent, docs, dedent):
+@_formats('enum-value-body -> Indent doc-line* attribute-line* Dedent')
+def _enum_value_body(indent, docs, attributes, dedent):
   del indent, dedent  # Unused
-  return _indent_rows(docs)
+  return _indent_rows(docs + attributes)
 
 
 @_formats('external-body -> Indent doc-line* attribute-line* Dedent')
diff --git a/compiler/front_end/format_emb_test.py b/compiler/front_end/format_emb_test.py
index a114315..e633f59 100644
--- a/compiler/front_end/format_emb_test.py
+++ b/compiler/front_end/format_emb_test.py
@@ -127,6 +127,7 @@
       "conditional_inline_bits_formatting",
       "dotted_names",
       "empty",
+      "enum_value_attributes",
       "enum_value_bodies",
       "enum_values_aligned",
       "equality_expressions",
diff --git a/compiler/front_end/module_ir.py b/compiler/front_end/module_ir.py
index 15312cf..94b7d8b 100644
--- a/compiler/front_end/module_ir.py
+++ b/compiler/front_end/module_ir.py
@@ -1136,22 +1136,24 @@
 
 # name = value
 @_handles('enum-value -> '
-          '    constant-name "=" expression doc? Comment? eol enum-value-body?')
-def _enum_value(name, equals, expression, documentation, comment, newline,
+          '    constant-name "=" expression attribute* doc? Comment? eol enum-value-body?')
+def _enum_value(name, equals, expression, attribute, documentation, comment, newline,
                 body):
   del equals, comment, newline  # Unused.
   result = ir_pb2.EnumValue(name=name,
                             value=expression,
-                            documentation=documentation.list)
+                            documentation=documentation.list,
+                            attribute=attribute.list)
   if body.list:
-    result.documentation.extend(body.list[0].list)
+    result.documentation.extend(body.list[0].documentation)
+    result.attribute.extend(body.list[0].attribute)
   return result
 
 
-@_handles('enum-value-body -> Indent doc-line* Dedent')
-def _enum_value_body(indent, docs, dedent):
+@_handles('enum-value-body -> Indent doc-line* attribute-line* Dedent')
+def _enum_value_body(indent, docs, attributes, dedent):
   del indent, dedent  # Unused.
-  return docs
+  return ir_pb2.EnumValue(documentation=docs.list, attribute=attributes.list)
 
 
 # An external is just a declaration that a type exists and has certain
diff --git a/compiler/front_end/module_ir_test.py b/compiler/front_end/module_ir_test.py
index 1700888..b4451ef 100644
--- a/compiler/front_end/module_ir_test.py
+++ b/compiler/front_end/module_ir_test.py
@@ -3852,6 +3852,98 @@
     }
   ]
 }
+
+===
+enum value attribute
+---
+enum Foo:
+  BAR     = 1 [test: 0]
+  BAZ     = 2
+    [test: 1]
+    [different: "test"]
+  FOO_BAR = 4
+    -- foo bar doc
+    [test: 2]
+  FOO_BAZ = 8 [test: 3] -- foo baz doc
+  BAR_FOO = 16 [test: 4]
+    -- bar foo doc
+  BAZ_FOO = 32 -- baz foo doc
+    [test: 5]
+---
+{
+  "type": [
+    {
+      "enumeration": {
+        "value": [
+          {
+            "name": { "name": { "text": "BAR" } },
+            "attribute": [
+              {
+                "name": { "text": "test" },
+                "value": { "expression": { "constant": { "value": "0" } } }
+              }
+            ]
+          },
+          {
+            "name": { "name": { "text": "BAZ" } },
+            "attribute": [
+              {
+                "name": { "text": "test" },
+                "value": { "expression": { "constant": { "value": "1" } } }
+              },
+              {
+                "name": { "text": "different" },
+                "value": { "string_constant": { "text": "test" } }
+              }
+            ]
+          },
+          {
+            "name": { "name": { "text": "FOO_BAR" } },
+            "documentation": [ { "text": "foo bar doc" } ],
+            "attribute": [
+              {
+                "name": { "text": "test" },
+                "value": { "expression": { "constant": { "value": "2" } } }
+              }
+            ]
+          },
+          {
+            "name": { "name": { "text": "FOO_BAZ" } },
+            "documentation": [ { "text": "foo baz doc" } ],
+            "attribute": [
+              {
+                "name": { "text": "test" },
+                "value": { "expression": { "constant": { "value": "3" } } }
+              }
+            ]
+          },
+          {
+            "name": { "name": { "text": "BAR_FOO" } },
+            "documentation": [ { "text": "bar foo doc" } ],
+            "attribute": [
+              {
+                "name": { "text": "test" },
+                "value": { "expression": { "constant": { "value": "4" } } }
+              }
+            ]
+          },
+          {
+            "name": { "name": { "text": "BAZ_FOO" } },
+            "documentation": [ { "text": "baz foo doc" } ],
+            "attribute": [
+              {
+                "name": { "text": "test" },
+                "value": { "expression": { "constant": { "value": "5" } } }
+              }
+            ]
+          }
+        ]
+      },
+      "name": { "name": { "text": "Foo" } }
+    }
+  ]
+}
+
 """
 
 
diff --git a/compiler/util/attribute_util.py b/compiler/util/attribute_util.py
index 80387bc..8d75de6 100644
--- a/compiler/util/attribute_util.py
+++ b/compiler/util/attribute_util.py
@@ -116,6 +116,7 @@
                            struct_attributes=None,
                            bits_attributes=None,
                            enum_attributes=None,
+                           enum_value_attributes=None,
                            external_attributes=None,
                            structure_virtual_field_attributes=None,
                            structure_physical_field_attributes=None):
@@ -143,6 +144,8 @@
         the attributes that are allowed at `bits` scope.
     enum_attributes: A set of (attribute_name, is_default) tuples specifying
         the attributes that are allowed at `enum` scope.
+    enum_value_attributes: A set of (attribute_name, is_default) tuples
+        specifying the attributes that are allowed at the scope of enum values.
     external_attributes: A set of (attribute_name, is_default) tuples
         specifying the attributes that are allowed at `external` scope.
     structure_virtual_field_attributes: A set of (attribute_name, is_default)
@@ -200,6 +203,11 @@
         "{}struct field '{}'".format(field_adjective, field.name.name.text),
         source_file_name))
 
+  def check_enum_value(value, source_file_name, errors):
+    errors.extend(_check_attributes(
+        value.attribute, types, back_end, enum_value_attributes,
+        "enum value '{}'".format(value.name.name.text), source_file_name))
+
   errors = []
   # TODO(bolms): Add a check that only known $default'ed attributes are
   # used.
@@ -212,6 +220,9 @@
   traverse_ir.fast_traverse_ir_top_down(
       ir, [ir_pb2.Field], check_struct_field,
       parameters={"errors": errors})
+  traverse_ir.fast_traverse_ir_top_down(
+      ir, [ir_pb2.EnumValue], check_enum_value,
+      parameters={"errors": errors})
   return errors
 
 
diff --git a/compiler/util/ir_pb2.py b/compiler/util/ir_pb2.py
index ef081bc..d1b5dd3 100644
--- a/compiler/util/ir_pb2.py
+++ b/compiler/util/ir_pb2.py
@@ -907,6 +907,7 @@
   name = Optional(NameDefinition)          # The name of the enum value.
   value = Optional(Expression)             # The value of the enum value.
   documentation = Repeated(Documentation)  # Value-specific documentation.
+  attribute = Repeated(Attribute)          # Value-specific attributes.
 
   source_location = Optional(Location)
 
diff --git a/doc/grammar.md b/doc/grammar.md
index a53abe6..55694bd 100644
--- a/doc/grammar.md
+++ b/doc/grammar.md
@@ -143,10 +143,16 @@
                                           enum-body
 enum-body                              -> Indent doc-line* attribute-line*
                                           enum-value+ Dedent
-enum-value                             -> constant-name "=" expression doc?
-                                          Comment? eol enum-value-body?
-enum-value-body                        -> Indent doc-line* Dedent
+enum-value                             -> constant-name "=" expression attribute*
+                                          doc? Comment? eol enum-value-body?
+enum-value-body                        -> Indent doc-line* attribute-line* Dedent
 doc                                    -> Documentation
+attribute                              -> "[" attribute-context? "$default"?
+                                          snake-word ":" attribute-value "]"
+attribute-value                        -> expression
+                                        | string-constant
+string-constant                        -> String
+attribute-context                      -> "(" snake-word ")"
 constant-name                          -> constant-word
 inline-bits-field-definition           -> field-location "bits" snake-name
                                           abbreviation? ":" Comment? eol
@@ -169,12 +175,6 @@
 field                                  -> field-location type snake-name
                                           abbreviation? attribute* doc? Comment?
                                           eol field-body?
-attribute                              -> "[" attribute-context? "$default"?
-                                          snake-word ":" attribute-value "]"
-attribute-value                        -> expression
-                                        | string-constant
-string-constant                        -> String
-attribute-context                      -> "(" snake-word ")"
 type                                   -> type-reference delimited-argument-list?
                                           type-size-specifier?
                                           array-length-specifier*
diff --git a/testdata/format/enum_value_attributes.emb b/testdata/format/enum_value_attributes.emb
new file mode 100644
index 0000000..d2afcd6
--- /dev/null
+++ b/testdata/format/enum_value_attributes.emb
@@ -0,0 +1,22 @@
+# 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 on `enum` values should be properly indented and spaced.
+
+enum Foo:
+  BAR = 0 [attribute: true]    -- Bar doc 1.
+    -- Bar doc 2.
+  BAZ = 1  [attribute: false]
+   -- Baz doc.
+   [different: "test"]
diff --git a/testdata/format/enum_value_attributes.emb.formatted b/testdata/format/enum_value_attributes.emb.formatted
new file mode 100644
index 0000000..b35b9c9
--- /dev/null
+++ b/testdata/format/enum_value_attributes.emb.formatted
@@ -0,0 +1,24 @@
+# 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 on `enum` values should be properly indented and spaced.
+
+
+enum Foo:
+  BAR = 0  [attribute: true]   -- Bar doc 1.
+    -- Bar doc 2.
+
+  BAZ = 1  [attribute: false]
+    -- Baz doc.
+    [different: "test"]
diff --git a/testdata/format/enum_value_attributes.emb.formatted_indent_4 b/testdata/format/enum_value_attributes.emb.formatted_indent_4
new file mode 100644
index 0000000..855192d
--- /dev/null
+++ b/testdata/format/enum_value_attributes.emb.formatted_indent_4
@@ -0,0 +1,24 @@
+# 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 on `enum` values should be properly indented and spaced.
+
+
+enum Foo:
+    BAR = 0  [attribute: true]   -- Bar doc 1.
+        -- Bar doc 2.
+
+    BAZ = 1  [attribute: false]
+        -- Baz doc.
+        [different: "test"]