- Start Date: 2023-11-27
- RFC PR: amaranth-lang/rfcs#31
- Amaranth Issue: amaranth-lang/amaranth#972
Enumeration type safety
Summary
Make Amaranth Enum and Flag use a custom ValueCastable view class, enforcing type safety.
Motivation
Python Enum provides an opaque wrapper over the underlying enum values,
providing type safety and guarding against improper usage in arithmetic
operations:
>>> from enum import Enum
>>> class EnumA(Enum):
... A = 0
... B = 1
...
>>> EnumA.A + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'EnumA' and 'int'
Likewise, Flag values can be used in bitwise operations, but only within
their own type:
>>> from enum import Flag
>>> class FlagA(Flag):
... A = 1
... B = 2
...
>>> class FlagB(Flag):
... C = 1
... D = 2
...
>>> FlagA.A | FlagA.B
<FlagA.A|B: 3>
>>> FlagA.A | FlagB.C
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'FlagA' and 'FlagB'
However, these safety properties are not currently enforced by Amaranth on enum-typed signals:
>>> from amaranth import *
>>> from amaranth.lib.enum import *
>>> class FlagA(Flag):
... A = 1
... B = 2
...
>>> class FlagB(Flag):
... C = 1
... D = 2
...
>>> a = Signal(FlagA)
>>> b = Signal(FlagB)
>>> a | b
(| (sig a) (sig b))
Guide-level explanation
Like in Python, Enum and Flag subclasses are considered strongly-typed,
while IntEnum and IntFlag are weakly-typed. Enum-typed Amaranth values
with strong typing are manipulated through amaranth.lib.enum.EnumView
and amaranth.lib.enum.FlagView classes, which wrap an underlying Value
in a type-safe container that only allows a small subset of operations.
For weakly-typed enums, Value is used directly, providing full
interchangeability with other values.
An EnumView or a FlagView can be obtained by:
- Creating an enum-typed signal (
a = Signal(MyEnum)) - Explicitly casting a value to the enum type (
MyEnum(value))
The operations available on EnumView and FlagView include:
- Comparing for equality to another view of the same enum type (
a == banda != b) - Assigning to or from a value
- Converting to a plain value via
Value.cast
The operations additionally available on FlagView include:
- Binary bitwise operations with another
FlagViewof the same type (a | b,a & b,a ^ b) - Bitwise inversion (
~a)
A custom subclass of EnumView or FlagView can be used for a given enum
type if so desired, by using the view_class keyword parameter on enum
creation.
Reference-level explanation
amaranth.lib.enum.EnumView is a ValueCastable subclass. The following
operations are defined on it:
EnumView(enum, value_castable): creates the viewshape(): returns the underlying enumas_value(): returns the underlying valueeq(value_castable): delegates toeqon the underlying value__eq__and__ne__: if the other argument is anEnumViewof the same enum type or a value of the enum type, delegates to the correspondingValueoperator; otherwise, raises aTypeError- All binary arithmetic, bitwise, and remaining comparison operators: raise
a
TypeError(to override the implementation provided byValuein case of an operation betweenEnumViewandValue)
amaranth.lib.enum.FlagView is a subclass of EnumView. The following
additional operations are defined on it:
__and__,__or__,__xor__: if the other argument is aFlagViewof the same enum type or a value of the enum type, delegates to the correspondingValueoperator and wraps the result inFlagView; otherwise, raises aTypeError__invert__: inverts all bits in this value corresponding to actually defined flags in the underlying enum type, then wraps the result inFlagView
The behavior of EnumMeta.__call__ when called on a value-castable
is changed as follows:
- If the enum has been created with a
view_class, the value-castable is wrapped in the given class - Otherwise, if the enum type is a subclass of
IntEnumorIntFlag, the value-castable is returned as a plainValue - Otherwise, if the enum type is a subclass of
Flag, the value-castable is wrapped inFlagView - Otherwise, the value-castable is wrapped in
EnumView
The behavior of EnumMeta.const is modified to go through the same logic.
Drawbacks
This proposal increases language complexity, and is not consistent with
eg. how amaranth.lib.data.View operates (which has much more lax type
checking).
Rationale and alternatives
Do nothing. Operations on mismatched types will continue to be silently allowed.
Equality could work more like Python equality (always returning false for mismatched types).
Assignment could be made strongly-typed as well (with corresponding hook
added to Value).
Prior art
This feature directly parallels the differences between Python's
Enum/Flag and IntEnum/IntFlag.
Unresolved questions
Instead of having an extension point via view_class, we could instead
automatically forward all otherwise unknown methods to the underlying enum
class, providing it the EnumView as self.
Future possibilities
None.