Skip to content

Commit 2c24d35

Browse files
committed
Add an RFC for enumeration type safety.
1 parent a3edeeb commit 2c24d35

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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

Comments
 (0)