Skip to content

Commit a54d3a2

Browse files
authored
RFC #52: Add amaranth.hdl.Choice, a pattern-based Value multiplexer
2 parents 4709748 + 1ebacb7 commit a54d3a2

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

text/0052-choice.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
- Start Date: 2024-07-01
2+
- RFC PR: [amaranth-lang/rfcs#52](https://github.com/amaranth-lang/rfcs/pull/52)
3+
- Amaranth Issue: [amaranth-lang/amaranth#1445](https://github.com/amaranth-lang/amaranth/issues/1445)
4+
5+
# Add `amaranth.hdl.Choice`, a pattern-based `Value` multiplexer
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
A new type of expression is added: `amaranth.hdl.Choice`. It is essentially a variant of `m.Switch`
11+
that returns a `Value` using the same patterns as `m.Case` for selection.
12+
13+
## Motivation
14+
[motivation]: #motivation
15+
16+
We currently have several multiplexer primitives:
17+
18+
- `Mux`, selecting from two values
19+
- `Array` indexing, selecting from multiple values by a simple index
20+
- `.bit_select` and `.word_select`, selecting from slices of a single value by a simple index
21+
- `m.Switch` together with combinatorial assignment to an intermediate `Signal`, selecting from multiple values by pattern matching
22+
23+
It is, however, not possible to select from multiple values by pattern matching without using an intermediate `Signal` and assignment (which can be a problem in contexts where a `Module` is not available). This RFC aims to close this hole.
24+
25+
This feature is generally useful and has been on the roadmap for a while. The immediate impulse for writing this RFC was using this functionality to implement string formatting for `lib.enum` values.
26+
27+
## Guide-level explanation
28+
[guide-level-explanation]: #guide-level-explanation
29+
30+
The `Choice` expression can be used to select from among several values via pattern matching:
31+
32+
```py
33+
abc = Signal(8)
34+
a = Signal(8)
35+
b = Signal(8)
36+
sel = Signal(4)
37+
m.d.comb += abc.eq(Choice(sel)
38+
# any pattern or tuple of patterns usable in `Value.matches` or `m.Case` is valid as key
39+
.case(1, a)
40+
.case(2, b)
41+
.case((3, 4), a + b)
42+
.case("11--", a - b)
43+
.case(("10--", "011-"), a * b)
44+
.default(13)
45+
)
46+
```
47+
48+
is equivalent to writing:
49+
50+
```py
51+
with m.Switch(sel):
52+
with m.Case(1):
53+
m.d.comb += abc.eq(a)
54+
with m.Case(2):
55+
m.d.comb += abc.eq(b)
56+
with m.Case(3, 4):
57+
m.d.comb += abc.eq(a + b)
58+
with m.Case("11--"):
59+
m.d.comb += abc.eq(a - b)
60+
with m.Case("10--", "011-"):
61+
m.d.comb += abc.eq(a * b)
62+
with m.Default():
63+
m.d.comb += abc.eq(13)
64+
```
65+
66+
`Choice` can also be used on the left-hand side of an assignment:
67+
68+
```py
69+
a = Signal(8)
70+
b = Signal(8)
71+
c = Signal(8)
72+
d = Signal(8)
73+
sel = Signal(2)
74+
m.d.sync += (Choice(sel)
75+
.case(0, a)
76+
.case(1, b)
77+
.case(2, c)
78+
.default(d)
79+
.eq(0))
80+
```
81+
82+
which is equivalent to:
83+
84+
```py
85+
with m.Switch(sel):
86+
with m.Case(0):
87+
m.d.sync += a.eq(0)
88+
with m.Case(1):
89+
m.d.sync += b.eq(0)
90+
with m.Case(2):
91+
m.d.sync += c.eq(0)
92+
with m.Default():
93+
m.d.sync += d.eq(0)
94+
```
95+
96+
If `default=` is not used, the default value is 0 when on right-hand side, and no assignment happens when on left-hand side.
97+
98+
In addition, `Mux` becomes assignable if the second and third argument are both assignable.
99+
100+
## Reference-level explanation
101+
[reference-level-explanation]: #reference-level-explanation
102+
103+
A new expression type is added:
104+
105+
- `amaranth.hdl.Choice(sel: ValueLike)`: creates a new `Choice` expression with no cases
106+
- `.case(self, patterns: int | str | tuple[int | str], value: ValueLike) -> Choice`: creates a new `Choice` based on this one, adding anoter case to it
107+
- `.default(self, value: ValueLike) -> Choice`: creates a new `Choice` based on this one, adding a default case to it
108+
109+
The expression evaluates `sel`, then matches it to `patterns` of every `.case()` in turn. If a match is found, the expression evaluates to the corresponding `value` of the first found match. If no match is found, the expression evaluates to the `value` of `.default()`, or to `Cat()` with no arguments if no `.default()` was used. The expression is assignable if all `.case()` values and `.default()` value (if any) are assignable.
110+
111+
Neither `.case()` nor `.default()` can be called on a `Choice` that already has a `.default()`.
112+
113+
The shape of the expression is determined as follows:
114+
115+
- if all `value` arguments are `ShapeCastable`, and it is the same `ShapeCastable` for all of them (as determined by `__eq__` on the `ShapeCastable`), the resulting value is transformed through `ShapeCastable.__call__` of that shape-castable
116+
- if all `value` arguments have a plain `Shape`, the minimum shape that can represent the shapes of all `cases` values and `default` (ie. determined the same as for `Array` proxy or `Mux` tree).
117+
- otherwise, an exception is raised
118+
119+
The default when `.default()` is not specified is `Cat()` to ensure the correct semantics for assignment (ie. discarding the assigned value). This also happens to provide the default 0 when on right-hand side.
120+
121+
`Choice` is also added to the Amaranth prelude.
122+
123+
In addition, the existing `Mux` expression is made valid on the left-hand side of an assignment, as if it was lowered as follows:
124+
125+
```py
126+
def Mux(sel, val1, val0):
127+
return Choice(a).case(0, val0).default(val1)
128+
```
129+
130+
`ArrayProxy` (ie. the type currently returned by `Array` indexing) is changed from a native `Value` to a `ValueCastable` that lowers to `Choice` (removing the odd case where we can currently build an invaid `Value`). To avoid problems with lowering the out-of-bounds case, the value returned for out-of-bounds `Array` accesses is changed to 0.
131+
132+
`__eq__` is added to the `ShapeCastable` protocol and documented (we already have suitable implementations in `lib.data` and `lib.enum`).
133+
134+
## Drawbacks
135+
[drawbacks]: #drawbacks
136+
137+
The language gets slightly more complex.
138+
139+
## Rationale and alternatives
140+
[rationale-and-alternatives]: #rationale-and-alternatives
141+
142+
The core functionality is fairly obvious. However, the syntax is not. Other possibilities include:
143+
144+
- `*args` (or perhaps iterable) of `(key, value)` tuples:
145+
146+
```py
147+
Choices(sel,
148+
(1, a),
149+
(2, b),
150+
((3, 4), c),
151+
("11--", d),
152+
default=e
153+
)
154+
```
155+
156+
- *args of newly-defined `amaranth.hdl.Case` object (not to be confused with `m.Case`):
157+
158+
```py
159+
Choices(sel,
160+
Case(1, a),
161+
Case(2, b),
162+
Case((3, 4), c),
163+
Case("11--", d),
164+
default=e,
165+
)
166+
```
167+
168+
The syntax proposed has been selected to have extension space (in the form of keyword arguments) for e.g. optional guard conditions.
169+
170+
## Prior art
171+
[prior-art]: #prior-art
172+
173+
This feature is inspired by Rust `match` construct.
174+
175+
## Unresolved questions
176+
[unresolved-questions]: #unresolved-questions
177+
178+
The name is subject to bikeshed. An obvious alternative is `Match`, though this RFC avoids using this name, as it suggests much more advanced pattern matching (with variable capture) than is currently available.
179+
180+
## Future possibilities
181+
[future-possibilities]: #future-possibilities
182+
183+
Optional guard conditions could be added to `Choice` and `m.Switch` cases (like Rust's `if` guards on `match` branches).

0 commit comments

Comments
 (0)