|
| 1 | +- Start Date: 2023-11-27 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#31](https://github.com/amaranth-lang/rfcs/pull/31) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#972](https://github.com/amaranth-lang/amaranth/issues/972) |
| 4 | + |
| 5 | +# Enumeration type safety |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Make Amaranth `Enum` and `Flag` use a custom `ValueCastable` view class, enforcing type safety. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +Python `Enum` provides an opaque wrapper over the underlying enum values, |
| 16 | +providing type safety and guarding against improper usage in arithmetic |
| 17 | +operations: |
| 18 | + |
| 19 | +```pycon |
| 20 | +>>> from enum import Enum |
| 21 | +>>> class EnumA(Enum): |
| 22 | +... A = 0 |
| 23 | +... B = 1 |
| 24 | +... |
| 25 | +>>> EnumA.A + 1 |
| 26 | +Traceback (most recent call last): |
| 27 | + File "<stdin>", line 1, in <module> |
| 28 | +TypeError: unsupported operand type(s) for +: 'EnumA' and 'int' |
| 29 | +``` |
| 30 | + |
| 31 | +Likewise, `Flag` values can be used in bitwise operations, but only within |
| 32 | +their own type: |
| 33 | + |
| 34 | +```pycon |
| 35 | +>>> from enum import Flag |
| 36 | +>>> class FlagA(Flag): |
| 37 | +... A = 1 |
| 38 | +... B = 2 |
| 39 | +... |
| 40 | +>>> class FlagB(Flag): |
| 41 | +... C = 1 |
| 42 | +... D = 2 |
| 43 | +... |
| 44 | +>>> FlagA.A | FlagA.B |
| 45 | +<FlagA.A|B: 3> |
| 46 | +>>> FlagA.A | FlagB.C |
| 47 | +Traceback (most recent call last): |
| 48 | + File "<stdin>", line 1, in <module> |
| 49 | +TypeError: unsupported operand type(s) for |: 'FlagA' and 'FlagB' |
| 50 | +``` |
| 51 | + |
| 52 | +However, these safety properties are not currently enforced by Amaranth |
| 53 | +on enum-typed signals: |
| 54 | + |
| 55 | +```pycon |
| 56 | +>>> from amaranth import * |
| 57 | +>>> from amaranth.lib.enum import * |
| 58 | +>>> class FlagA(Flag): |
| 59 | +... A = 1 |
| 60 | +... B = 2 |
| 61 | +... |
| 62 | +>>> class FlagB(Flag): |
| 63 | +... C = 1 |
| 64 | +... D = 2 |
| 65 | +... |
| 66 | +>>> a = Signal(FlagA) |
| 67 | +>>> b = Signal(FlagB) |
| 68 | +>>> a | b |
| 69 | +(| (sig a) (sig b)) |
| 70 | +``` |
| 71 | + |
| 72 | + |
| 73 | +## Guide-level explanation |
| 74 | +[guide-level-explanation]: #guide-level-explanation |
| 75 | + |
| 76 | +Like in Python, `Enum` and `Flag` subclasses are considered strongly-typed, |
| 77 | +while `IntEnum` and `IntFlag` are weakly-typed. Enum-typed Amaranth values |
| 78 | +with strong typing are manipulated through `amaranth.lib.enum.EnumView` |
| 79 | +and `amaranth.lib.enum.FlagView` classes, which wrap an underlying `Value` |
| 80 | +in a type-safe container that only allows a small subset of operations. |
| 81 | +For weakly-typed enums, `Value` is used directly, providing full |
| 82 | +interchangeability with other values. |
| 83 | + |
| 84 | +An `EnumView` or a `FlagView` can be obtained by: |
| 85 | + |
| 86 | +- Creating an enum-typed signal (`a = Signal(MyEnum)`) |
| 87 | +- Explicitly casting a value to the enum type (`MyEnum(value)`) |
| 88 | + |
| 89 | +The operations available on `EnumView` and `FlagView` include: |
| 90 | + |
| 91 | +- Comparing for equality to another view of the same enum type (`a == b` and `a != b`) |
| 92 | +- Assigning to or from a value |
| 93 | +- Converting to a plain value via `Value.cast` |
| 94 | + |
| 95 | +The operations additionally available on `FlagView` include: |
| 96 | + |
| 97 | +- Binary bitwise operations with another `FlagView` of the same type |
| 98 | + (`a | b`, `a & b`, `a ^ b`) |
| 99 | +- Bitwise inversion (`~a`) |
| 100 | + |
| 101 | +A custom subclass of `EnumView` or `FlagView` can be used for a given enum |
| 102 | +type if so desired, by using the `view_class` keyword parameter on enum |
| 103 | +creation. |
| 104 | + |
| 105 | +## Reference-level explanation |
| 106 | +[reference-level-explanation]: #reference-level-explanation |
| 107 | + |
| 108 | +`amaranth.lib.enum.EnumView` is a `ValueCastable` subclass. The following |
| 109 | +operations are defined on it: |
| 110 | + |
| 111 | +- `EnumView(enum, value_castable)`: creates the view |
| 112 | +- `shape()`: returns the underlying enum |
| 113 | +- `as_value()`: returns the underlying value |
| 114 | +- `eq(value_castable)`: delegates to `eq` on the underlying value |
| 115 | +- `__eq__` and `__ne__`: if the other argument is an `EnumView` of the same |
| 116 | + enum type or a value of the enum type, delegates to the corresponding |
| 117 | + `Value` operator; otherwise, raises a `TypeError` |
| 118 | +- All binary arithmetic, bitwise, and remaining comparison operators: raise |
| 119 | + a `TypeError` (to override the implementation provided by `Value` in case |
| 120 | + of an operation between `EnumView` and `Value`) |
| 121 | + |
| 122 | +`amaranth.lib.enum.FlagView` is a subclass of `EnumView`. The following |
| 123 | +additional operations are defined on it: |
| 124 | + |
| 125 | +- `__and__`, `__or__`, `__xor__`: if the other argument is a `FlagView` |
| 126 | + of the same enum type or a value of the enum type, delegates to the |
| 127 | + corresponding `Value` operator and wraps the result in `FlagView`; |
| 128 | + otherwise, raises a `TypeError` |
| 129 | +- `__invert__`: inverts all bits in this value corresponding to actually |
| 130 | + defined flags in the underlying enum type, then wraps the result in |
| 131 | + `FlagView` |
| 132 | + |
| 133 | +The behavior of `EnumMeta.__call__` when called on a value-castable |
| 134 | +is changed as follows: |
| 135 | + |
| 136 | +- If the enum has been created with a `view_class`, the value-castable |
| 137 | + is wrapped in the given class |
| 138 | +- Otherwise, if the enum type is a subclass of `IntEnum` or `IntFlag`, the |
| 139 | + value-castable is returned as a plain `Value` |
| 140 | +- Otherwise, if the enum type is a subclass of `Flag`, the value-castable |
| 141 | + is wrapped in `FlagView` |
| 142 | +- Otherwise, the value-castable is wrapped in `EnumView` |
| 143 | + |
| 144 | +The behavior of `EnumMeta.const` is modified to go through the same logic. |
| 145 | + |
| 146 | +## Drawbacks |
| 147 | +[drawbacks]: #drawbacks |
| 148 | + |
| 149 | +This proposal increases language complexity, and is not consistent with |
| 150 | +eg. how `amaranth.lib.data.View` operates (which has much more lax type |
| 151 | +checking). |
| 152 | + |
| 153 | +## Rationale and alternatives |
| 154 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 155 | + |
| 156 | +Do nothing. Operations on mismatched types will continue to be silently |
| 157 | +allowed. |
| 158 | + |
| 159 | +Equality could work more like Python equality (always returning false |
| 160 | +for mismatched types). |
| 161 | + |
| 162 | +Assignment could be made strongly-typed as well (with corresponding hook |
| 163 | +added to `Value`). |
| 164 | + |
| 165 | +## Prior art |
| 166 | +[prior-art]: #prior-art |
| 167 | + |
| 168 | +This feature directly parallels the differences between Python's |
| 169 | +`Enum`/`Flag` and `IntEnum`/`IntFlag`. |
| 170 | + |
| 171 | +## Unresolved questions |
| 172 | +[unresolved-questions]: #unresolved-questions |
| 173 | + |
| 174 | +Instead of having an extension point via `view_class`, we could instead |
| 175 | +automatically forward all otherwise unknown methods to the underlying enum |
| 176 | +class, providing it the `EnumView` as `self`. |
| 177 | + |
| 178 | +## Future possibilities |
| 179 | +[future-possibilities]: #future-possibilities |
| 180 | + |
| 181 | +None. |
0 commit comments