Add support for `maximum_bits` and `is_signed` on `enum`s.
diff --git a/compiler/front_end/attribute_checker.py b/compiler/front_end/attribute_checker.py
index 92dd939..27e6722 100644
--- a/compiler/front_end/attribute_checker.py
+++ b/compiler/front_end/attribute_checker.py
@@ -35,6 +35,10 @@
_MUST_BE_CONSTANT_MESSAGE = "Attribute '{name}' must have a constant value."
+# Default value for maximum_bits on an `enum`.
+_DEFAULT_ENUM_MAXIMUM_BITS = 64
+
+
# Attribute type checkers
def _is_constant_boolean(attr, module_source_file):
"""Checks if the given attr is a constant boolean."""
@@ -108,8 +112,10 @@
_ATTRIBUTE_TYPES = {
("", attributes.ADDRESSABLE_UNIT_SIZE): _is_constant_integer,
("", attributes.BYTE_ORDER): _is_valid_byte_order,
+ ("", attributes.ENUM_MAXIMUM_BITS): _is_constant_integer,
("", attributes.FIXED_SIZE): _is_constant_integer,
("", attributes.IS_INTEGER): _is_constant_boolean,
+ ("", attributes.IS_SIGNED): _is_constant_boolean,
("", attributes.REQUIRES): _is_boolean,
("", attributes.STATIC_REQUIREMENTS): _is_boolean,
("", attributes.TEXT_OUTPUT): _is_valid_text_output,
@@ -125,16 +131,15 @@
_BITS_ATTRIBUTES = {
("", attributes.FIXED_SIZE, False),
("", attributes.REQUIRES, False),
- ("", attributes.STATIC_REQUIREMENTS, False),
}
_STRUCT_ATTRIBUTES = {
("", attributes.FIXED_SIZE, False),
("", attributes.BYTE_ORDER, True),
("", attributes.REQUIRES, False),
- ("", attributes.STATIC_REQUIREMENTS, False),
}
_ENUM_ATTRIBUTES = {
- ("", attributes.STATIC_REQUIREMENTS, False),
+ ("", attributes.ENUM_MAXIMUM_BITS, False),
+ ("", attributes.IS_SIGNED, False),
}
_EXTERNAL_ATTRIBUTES = {
("", attributes.ADDRESSABLE_UNIT_SIZE, False),
@@ -166,7 +171,23 @@
maximum_value=str(value))),
source_location=source_location),
source_location=source_location)
- return ir_pb2.Attribute(name=ir_pb2.Word(text=name),
+ return ir_pb2.Attribute(name=ir_pb2.Word(text=name,
+ source_location=source_location),
+ value=attr_value,
+ source_location=source_location)
+
+
+def _construct_boolean_attribute(name, value, source_location):
+ """Constructs a boolean Attribute with the given name and value."""
+ attr_value = ir_pb2.AttributeValue(
+ expression=ir_pb2.Expression(
+ boolean_constant=ir_pb2.BooleanConstant(
+ value=value, source_location=source_location),
+ type=ir_pb2.ExpressionType(boolean=ir_pb2.BooleanType(value=value)),
+ source_location=source_location),
+ source_location=source_location)
+ return ir_pb2.Attribute(name=ir_pb2.Word(text=name,
+ source_location=source_location),
value=attr_value,
source_location=source_location)
@@ -425,6 +446,30 @@
# _verify_addressable_unit_attribute_on_external, below.
+def _add_missing_width_and_sign_attributes_on_enum(enum, type_definition):
+ """Sets the maximum_bits and is_signed attributes for an enum, if needed."""
+ max_bits_attr = ir_util.get_integer_attribute(type_definition.attribute,
+ attributes.ENUM_MAXIMUM_BITS)
+ if max_bits_attr is None:
+ type_definition.attribute.extend([
+ _construct_integer_attribute(attributes.ENUM_MAXIMUM_BITS,
+ _DEFAULT_ENUM_MAXIMUM_BITS,
+ type_definition.source_location)])
+ signed_attr = ir_util.get_boolean_attribute(type_definition.attribute,
+ attributes.IS_SIGNED)
+ if signed_attr is None:
+ for value in enum.value:
+ numeric_value = ir_util.constant_value(value.value)
+ if numeric_value < 0:
+ is_signed = True
+ break
+ else:
+ is_signed = False
+ type_definition.attribute.extend([
+ _construct_boolean_attribute(attributes.IS_SIGNED, is_signed,
+ type_definition.source_location)])
+
+
def _verify_byte_order_attribute_on_field(field, type_definition,
source_file_name, ir, errors):
"""Verifies the byte_order attribute on the given field."""
@@ -496,6 +541,22 @@
])
+def _verify_width_attribute_on_enum(enum, type_definition, source_file_name,
+ errors):
+ """Verifies the maximum_bits attribute for an enum TypeDefinition."""
+ max_bits_value = ir_util.get_integer_attribute(type_definition.attribute,
+ attributes.ENUM_MAXIMUM_BITS)
+ # The attribute should already have been defaulted, if not originally present.
+ assert max_bits_value is not None, "maximum_bits not set"
+ if max_bits_value > 64 or max_bits_value < 1:
+ max_bits_attr = ir_util.get_attribute(type_definition.attribute,
+ attributes.ENUM_MAXIMUM_BITS)
+ errors.append([
+ error.error(source_file_name, max_bits_attr.source_location,
+ "'maximum_bits' on an 'enum' must be between 1 and 64.")
+ ])
+
+
def _gather_default_attributes(obj, defaults):
defaults = defaults.copy()
for attr in obj.attribute:
@@ -512,6 +573,8 @@
traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.External], _add_addressable_unit_to_external)
traverse_ir.fast_traverse_ir_top_down(
+ ir, [ir_pb2.Enum], _add_missing_width_and_sign_attributes_on_enum)
+ 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,
@@ -544,6 +607,9 @@
ir, [ir_pb2.Structure], _verify_size_attributes_on_structure,
parameters={"errors": errors})
traverse_ir.fast_traverse_ir_top_down(
+ ir, [ir_pb2.Enum], _verify_width_attribute_on_enum,
+ parameters={"errors": errors})
+ traverse_ir.fast_traverse_ir_top_down(
ir, [ir_pb2.External], _verify_addressable_unit_attribute_on_external,
parameters={"errors": errors})
traverse_ir.fast_traverse_ir_top_down(
diff --git a/compiler/front_end/attribute_checker_test.py b/compiler/front_end/attribute_checker_test.py
index d57d602..efae0e6 100644
--- a/compiler/front_end/attribute_checker_test.py
+++ b/compiler/front_end/attribute_checker_test.py
@@ -26,6 +26,8 @@
# of the contract with back ends.
_BYTE_ORDER = "byte_order"
_FIXED_SIZE = "fixed_size_in_bits"
+_IS_SIGNED = "is_signed"
+_MAX_BITS = "maximum_bits"
def _make_ir_from_emb(emb_text, name="m.emb"):
@@ -584,6 +586,75 @@
"enumeration, or boolean fields.")]],
error.filter_errors(attribute_checker.normalize_and_verify(ir)))
+ def test_adds_false_is_signed_attribute(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " ZERO = 0\n")
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+ enum = ir.module[0].type[0]
+ is_signed_attr = ir_util.get_attribute(enum.attribute, _IS_SIGNED)
+ self.assertTrue(is_signed_attr.expression.HasField("boolean_constant"))
+ self.assertFalse(is_signed_attr.expression.boolean_constant.value)
+
+ def test_leaves_is_signed_attribute(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " [is_signed: true]\n"
+ " ZERO = 0\n")
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+ enum = ir.module[0].type[0]
+ is_signed_attr = ir_util.get_attribute(enum.attribute, _IS_SIGNED)
+ self.assertTrue(is_signed_attr.expression.HasField("boolean_constant"))
+ self.assertTrue(is_signed_attr.expression.boolean_constant.value)
+
+ def test_adds_true_is_signed_attribute(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " NEGATIVE_ONE = -1\n")
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+ enum = ir.module[0].type[0]
+ is_signed_attr = ir_util.get_attribute(enum.attribute, _IS_SIGNED)
+ self.assertTrue(is_signed_attr.expression.HasField("boolean_constant"))
+ self.assertTrue(is_signed_attr.expression.boolean_constant.value)
+
+ def test_adds_max_bits_attribute(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " ZERO = 0\n")
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+ enum = ir.module[0].type[0]
+ max_bits_attr = ir_util.get_attribute(enum.attribute, _MAX_BITS)
+ self.assertTrue(max_bits_attr.expression.HasField("constant"))
+ self.assertEqual("64", max_bits_attr.expression.constant.value)
+
+ def test_leaves_max_bits_attribute(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " [maximum_bits: 32]\n"
+ " ZERO = 0\n")
+ self.assertEqual([], attribute_checker.normalize_and_verify(ir))
+ enum = ir.module[0].type[0]
+ max_bits_attr = ir_util.get_attribute(enum.attribute, _MAX_BITS)
+ self.assertTrue(max_bits_attr.expression.HasField("constant"))
+ self.assertEqual("32", max_bits_attr.expression.constant.value)
+
+ def test_rejects_too_small_max_bits(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " [maximum_bits: 0]\n"
+ " ZERO = 0\n")
+ attribute_ir = ir.module[0].type[0].attribute[0]
+ self.assertEqual(
+ [[error.error(
+ "m.emb", attribute_ir.value.source_location,
+ "'maximum_bits' on an 'enum' must be between 1 and 64.")]],
+ error.filter_errors(attribute_checker.normalize_and_verify(ir)))
+
+ def test_rejects_too_large_max_bits(self):
+ ir = _make_ir_from_emb("enum Foo:\n"
+ " [maximum_bits: 65]\n"
+ " ZERO = 0\n")
+ attribute_ir = ir.module[0].type[0].attribute[0]
+ self.assertEqual(
+ [[error.error(
+ "m.emb", attribute_ir.value.source_location,
+ "'maximum_bits' on an 'enum' must be between 1 and 64.")]],
+ error.filter_errors(attribute_checker.normalize_and_verify(ir)))
+
if __name__ == "__main__":
unittest.main()
diff --git a/compiler/front_end/attributes.py b/compiler/front_end/attributes.py
index fd6e31c..f931f5b 100644
--- a/compiler/front_end/attributes.py
+++ b/compiler/front_end/attributes.py
@@ -23,3 +23,5 @@
REQUIRES = "requires"
STATIC_REQUIREMENTS = "static_requirements"
TEXT_OUTPUT = "text_output"
+ENUM_MAXIMUM_BITS = "maximum_bits"
+IS_SIGNED = "is_signed"
diff --git a/compiler/front_end/constraints.py b/compiler/front_end/constraints.py
index 9af1b17..6beaf31 100644
--- a/compiler/front_end/constraints.py
+++ b/compiler/front_end/constraints.py
@@ -134,30 +134,29 @@
])
-def _check_that_enum_values_are_representable(enum_type, source_file_name,
- errors):
- """Checks that enumeration values can fit in an int64 or uint64."""
+def _check_that_enum_values_are_representable(enum_type, type_definition,
+ source_file_name, errors):
+ """Checks that enumeration values can fit in their specified int type."""
values = []
+ max_enum_size = ir_util.get_integer_attribute(
+ type_definition.attribute, attributes.ENUM_MAXIMUM_BITS)
+ is_signed = ir_util.get_boolean_attribute(
+ type_definition.attribute, attributes.IS_SIGNED)
+ if is_signed:
+ enum_range = (-(2**(max_enum_size-1)), 2**(max_enum_size-1)-1)
+ else:
+ enum_range = (0, 2**max_enum_size-1)
for value in enum_type.value:
values.append((ir_util.constant_value(value.value), value))
- # Guess if the user intended a signed or unsigned enumeration based on how
- # many values would be out of range given either type.
- signed_out_of_range = [v for v in values if not -2**63 <= v[0] <= 2**63-1]
- unsigned_out_of_range = [v for v in values if not 0 <= v[0] <= 2**64-1]
- if len(signed_out_of_range) < len(unsigned_out_of_range):
- out_of_range = signed_out_of_range
- range_name = "signed "
- else:
- out_of_range = unsigned_out_of_range
- range_name = "unsigned "
- # If all values are in range for either a signed or an unsigned enumeration,
- # this loop will have zero iterations.
+ out_of_range = [v for v in values
+ if not enum_range[0] <= v[0] <= enum_range[1]]
+ # If all values are in range, this loop will have zero iterations.
for value in out_of_range:
errors.append([
error.error(
source_file_name, value[1].value.source_location,
- "Value {} is out of range for {}enumeration.".format(
- value[0], range_name if -2**63 <= value[0] <= 2**64-1 else ""))
+ "Value {} is out of range for {}-bit {} enumeration.".format(
+ value[0], max_enum_size, "signed" if is_signed else "unsigned"))
])
@@ -297,10 +296,6 @@
"""Checks that the given atomic `type_ir` is allowed to be `size` bits."""
referenced_type_definition = ir_util.find_object(
type_ir.atomic_type.reference, ir)
- # TODO(bolms): replace this with a check against an automatically-generated
- # `static_requirements` attribute on enum types. (The main problem is that
- # the generated attribute would have no source text, so there would be a crash
- # when trying to display the error.)
if referenced_type_definition.HasField("enumeration"):
if size is None:
return [[
@@ -309,14 +304,18 @@
"Enumeration {} cannot be placed in a dynamically-sized "
"field.".format(_render_type(type_ir, ir)))
]]
- elif size < 1 or size > 64:
- return [[
- error.error(
- source_file_name, type_ir.source_location,
- "Enumeration {} cannot be {} bits; enumerations must be between "
- "1 and 64 bits, inclusive.".format(
- _render_atomic_type_name(type_ir, ir), size))
- ]]
+ else:
+ max_enum_size = ir_util.get_integer_attribute(
+ referenced_type_definition.attribute, attributes.ENUM_MAXIMUM_BITS)
+ if size < 1 or size > max_enum_size:
+ return [[
+ error.error(
+ source_file_name, type_ir.source_location,
+ "Enumeration {} cannot be {} bits; {} must be between "
+ "1 and {} bits, inclusive.".format(
+ _render_atomic_type_name(type_ir, ir), size,
+ _render_atomic_type_name(type_ir, ir), max_enum_size))
+ ]]
if size is None:
bindings = {"$is_statically_sized": False}
diff --git a/compiler/front_end/constraints_test.py b/compiler/front_end/constraints_test.py
index ff33586..9bf5a8e 100644
--- a/compiler/front_end/constraints_test.py
+++ b/compiler/front_end/constraints_test.py
@@ -282,7 +282,7 @@
error_type = ir.module[0].type[0].structure.field[0].type
self.assertEqual([[
error.error("m.emb", error_type.source_location,
- "Enumeration type 'Bar' cannot be 0 bits; enumerations "
+ "Enumeration type 'Bar' cannot be 0 bits; type 'Bar' "
"must be between 1 and 64 bits, inclusive."),
]], error.filter_errors(constraints.check_constraints(ir)))
@@ -331,10 +331,24 @@
error_type = ir.module[0].type[0].structure.field[0].type
self.assertEqual([[
error.error("m.emb", error_type.source_location,
- "Enumeration type 'Bar' cannot be 72 bits; enumerations "
+ "Enumeration type 'Bar' cannot be 72 bits; type 'Bar' " +
"must be between 1 and 64 bits, inclusive."),
]], error.filter_errors(constraints.check_constraints(ir)))
+ def test_explicit_enumeration_size_too_big_for_small_enum(self):
+ ir = _make_ir_from_emb('[$default byte_order: "BigEndian"]\n'
+ "struct Foo:\n"
+ " 0 [+8] Bar sixty_four_bit\n"
+ "enum Bar:\n"
+ " [maximum_bits: 63]\n"
+ " BAZ = 0\n")
+ error_type = ir.module[0].type[0].structure.field[0].type
+ self.assertEqual([[
+ error.error("m.emb", error_type.source_location,
+ "Enumeration type 'Bar' cannot be 64 bits; type 'Bar' " +
+ "must be between 1 and 63 bits, inclusive."),
+ ]], error.filter_errors(constraints.check_constraints(ir)))
+
def test_explicit_size_on_fixed_size_type(self):
ir = _make_ir_from_emb("struct Foo:\n"
" 0 [+1] Byte:8 one_byte\n"
@@ -417,7 +431,8 @@
# TODO(bolms): Try to print numbers like 2**64 in hex? (I.e., if a
# number is a round number in hex, but not in decimal, print in
# hex?)
- "Value 18446744073709551616 is out of range for enumeration.")]
+ "Value 18446744073709551616 is out of range for 64-bit unsigned " +
+ "enumeration.")]
], constraints.check_constraints(ir))
def test_enum_value_too_low(self):
@@ -428,7 +443,8 @@
self.assertEqual([
[error.error(
"m.emb", error_value.source_location,
- "Value -9223372036854775809 is out of range for enumeration.")]
+ "Value -9223372036854775809 is out of range for 64-bit signed " +
+ "enumeration.")]
], constraints.check_constraints(ir))
def test_enum_value_too_wide(self):
@@ -436,11 +452,12 @@
"enum Foo:\n"
" LOW = -1\n"
" HIGH = 0x8000_0000_0000_0000\n")
- error_value = ir.module[0].type[0].enumeration.value[0].value
+ error_value = ir.module[0].type[0].enumeration.value[1].value
self.assertEqual([[
error.error(
"m.emb", error_value.source_location,
- "Value -1 is out of range for unsigned enumeration.")
+ "Value 9223372036854775808 is out of range for 64-bit signed " +
+ "enumeration.")
]], error.filter_errors(constraints.check_constraints(ir)))
def test_enum_value_too_wide_unsigned_error_message(self):
@@ -453,7 +470,33 @@
self.assertEqual([[
error.error(
"m.emb", error_value.source_location,
- "Value 9223372036854775808 is out of range for signed enumeration.")
+ "Value 9223372036854775808 is out of range for 64-bit signed " +
+ "enumeration.")
+ ]], error.filter_errors(constraints.check_constraints(ir)))
+
+ def test_enum_value_too_wide_small_size_error_message(self):
+ ir = _make_ir_from_emb('[$default byte_order: "LittleEndian"]\n'
+ "enum Foo:\n"
+ " [maximum_bits: 8]\n"
+ " HIGH = 0x100\n")
+ error_value = ir.module[0].type[0].enumeration.value[0].value
+ self.assertEqual([[
+ error.error(
+ "m.emb", error_value.source_location,
+ "Value 256 is out of range for 8-bit unsigned enumeration.")
+ ]], error.filter_errors(constraints.check_constraints(ir)))
+
+ def test_enum_value_too_wide_small_size_signed_error_message(self):
+ ir = _make_ir_from_emb('[$default byte_order: "LittleEndian"]\n'
+ "enum Foo:\n"
+ " [maximum_bits: 8]\n"
+ " [is_signed: true]\n"
+ " HIGH = 0x80\n")
+ error_value = ir.module[0].type[0].enumeration.value[0].value
+ self.assertEqual([[
+ error.error(
+ "m.emb", error_value.source_location,
+ "Value 128 is out of range for 8-bit signed enumeration.")
]], error.filter_errors(constraints.check_constraints(ir)))
def test_enum_value_too_wide_multiple(self):
@@ -463,15 +506,17 @@
" LOW2 = -1\n"
" HIGH = 0x8000_0000_0000_0000\n"
" HIGH2 = 0x8000_0000_0000_0001\n")
- error_value = ir.module[0].type[0].enumeration.value[0].value
- error_value2 = ir.module[0].type[0].enumeration.value[1].value
+ error_value = ir.module[0].type[0].enumeration.value[2].value
+ error_value2 = ir.module[0].type[0].enumeration.value[3].value
self.assertEqual([
[error.error(
"m.emb", error_value.source_location,
- "Value -2 is out of range for unsigned enumeration.")],
+ "Value 9223372036854775808 is out of range for 64-bit signed " +
+ "enumeration.")],
[error.error(
"m.emb", error_value2.source_location,
- "Value -1 is out of range for unsigned enumeration.")]
+ "Value 9223372036854775809 is out of range for 64-bit signed " +
+ "enumeration.")]
], error.filter_errors(constraints.check_constraints(ir)))
def test_enum_value_too_wide_multiple_signed_error_message(self):
@@ -487,11 +532,11 @@
self.assertEqual([
[error.error(
"m.emb", error_value.source_location,
- "Value 9223372036854775808 is out of range for signed "
+ "Value 9223372036854775808 is out of range for 64-bit signed "
"enumeration.")],
[error.error(
"m.emb", error_value2.source_location,
- "Value 9223372036854775809 is out of range for signed "
+ "Value 9223372036854775809 is out of range for 64-bit signed "
"enumeration.")]
], error.filter_errors(constraints.check_constraints(ir)))
@@ -501,15 +546,55 @@
" LOW = -1\n"
" HIGH = 0x8000_0000_0000_0000\n"
" HIGH2 = 0x1_0000_0000_0000_0000\n")
- error_value = ir.module[0].type[0].enumeration.value[0].value
+ error_value1 = ir.module[0].type[0].enumeration.value[1].value
error_value2 = ir.module[0].type[0].enumeration.value[2].value
self.assertEqual([
[error.error(
- "m.emb", error_value.source_location,
- "Value -1 is out of range for unsigned enumeration.")],
+ "m.emb", error_value1.source_location,
+ "Value 9223372036854775808 is out of range for 64-bit signed " +
+ "enumeration.")],
[error.error(
"m.emb", error_value2.source_location,
- "Value 18446744073709551616 is out of range for enumeration.")]
+ "Value 18446744073709551616 is out of range for 64-bit signed " +
+ "enumeration.")]
+ ], error.filter_errors(constraints.check_constraints(ir)))
+
+ def test_enum_value_explicitly_signed_error_message(self):
+ ir = _make_ir_from_emb('[$default byte_order: "LittleEndian"]\n'
+ "enum Foo:\n"
+ " [is_signed: true]\n"
+ " HIGH = 0x8000_0000_0000_0000\n"
+ " HIGH2 = 0x1_0000_0000_0000_0000\n")
+ error_value0 = ir.module[0].type[0].enumeration.value[0].value
+ error_value1 = ir.module[0].type[0].enumeration.value[1].value
+ self.assertEqual([
+ [error.error(
+ "m.emb", error_value0.source_location,
+ "Value 9223372036854775808 is out of range for 64-bit signed " +
+ "enumeration.")],
+ [error.error(
+ "m.emb", error_value1.source_location,
+ "Value 18446744073709551616 is out of range for 64-bit signed " +
+ "enumeration.")]
+ ], error.filter_errors(constraints.check_constraints(ir)))
+
+ def test_enum_value_explicitly_unsigned_error_message(self):
+ ir = _make_ir_from_emb('[$default byte_order: "LittleEndian"]\n'
+ "enum Foo:\n"
+ " [is_signed: false]\n"
+ " LOW = -1\n"
+ " HIGH = 0x8000_0000_0000_0000\n"
+ " HIGH2 = 0x1_0000_0000_0000_0000\n")
+ error_value0 = ir.module[0].type[0].enumeration.value[0].value
+ error_value2 = ir.module[0].type[0].enumeration.value[2].value
+ self.assertEqual([
+ [error.error(
+ "m.emb", error_value0.source_location,
+ "Value -1 is out of range for 64-bit unsigned enumeration.")],
+ [error.error(
+ "m.emb", error_value2.source_location,
+ "Value 18446744073709551616 is out of range for 64-bit unsigned " +
+ "enumeration.")]
], error.filter_errors(constraints.check_constraints(ir)))
def test_explicit_non_byte_size_array_element(self):