-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
- Start Date: 2024-03-11 | ||
- RFC PR: [amaranth-lang/rfcs#52](https://github.com/amaranth-lang/rfcs/pull/52) | ||
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) | ||
|
||
# Add `amaranth.hdl.Choice`, a pattern-based `Value` multiplexer | ||
|
||
## Summary | ||
[summary]: #summary | ||
|
||
A new type of expression is added: `amaranth.hdl.Choice`. It is essentially a variant of `m.Switch` | ||
that returns a `Value` using the same patterns as `m.Case` for selection. | ||
|
||
## Motivation | ||
[motivation]: #motivation | ||
|
||
We currently have several multiplexer primitives: | ||
|
||
- `Mux`, selecting from two values | ||
- `Array` indexing, selecting from multiple values by a simple index | ||
- `.bit_select` and `.word_select`, selecting from slices of a single value by a simple index | ||
- `m.Switch` together with combinatorial assignment to an intermediate `Signal`, selecting from multiple values by pattern matching | ||
|
||
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. | ||
|
||
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. | ||
|
||
## Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
The `Choice` expression can be used to select from among several values via pattern matching: | ||
|
||
```py | ||
abc = Signal(8) | ||
a = Signal(8) | ||
b = Signal(8) | ||
sel = Signal(4) | ||
m.d.comb += abc.eq(Choice(sel, { | ||
# any pattern or tuple of patterns usable in `Value.matches` or `m.Case` is valid as key | ||
1: a, | ||
2: b, | ||
(3, 4): a + b, | ||
"11--": a - b, | ||
("10--", "011-"): a * b, | ||
}, default=13)) | ||
``` | ||
|
||
is equivalent to writing: | ||
|
||
```py | ||
with m.Switch(sel): | ||
with m.Case(1): | ||
m.d.comb += abc.eq(a) | ||
with m.Case(2): | ||
m.d.comb += abc.eq(b) | ||
with m.Case(3, 4): | ||
m.d.comb += abc.eq(a + b) | ||
with m.Case("11--"): | ||
m.d.comb += abc.eq(a - b) | ||
with m.Case("10--", "011-"): | ||
m.d.comb += abc.eq(a * b) | ||
with m.Default(): | ||
m.d.comb += abc.eq(13) | ||
``` | ||
|
||
`Choice` can also be used on the left-hand side of an assignment: | ||
|
||
```py | ||
a = Signal(8) | ||
b = Signal(8) | ||
c = Signal(8) | ||
d = Signal(8) | ||
sel = Signal(2) | ||
m.d.sync += Choice(sel, { | ||
0: a, | ||
1: b, | ||
2: c, | ||
}, default=d).eq(0) | ||
``` | ||
|
||
which is equivalent to: | ||
|
||
```py | ||
with m.Switch(sel): | ||
with m.Case(0): | ||
m.d.sync += a.eq(0) | ||
with m.Case(1): | ||
m.d.sync += b.eq(0) | ||
with m.Case(2): | ||
m.d.sync += c.eq(0) | ||
with m.Default(): | ||
m.d.sync += d.eq(0) | ||
``` | ||
|
||
If `default=` is not used, the default value is 0 when on right-hand side, and no assignment happens when on left-hand side. | ||
|
||
In addition, `Mux` becomes assignable if the second and third argument are both assignable. | ||
|
||
## Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
A new expression type is added: | ||
|
||
- `amaranth.hdl.Choice(sel: Value, cases: Mapping[int | str | tuple[int | str], Value], *, default: Value = Cat())` | ||
|
||
The expression evaluates `sel`, then matches it to every key of the `cases` dict 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 `default`. The expression is assignable if all `cases` values and `default` are assignable. | ||
|
||
The shape of the expression is 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). | ||
|
||
The default `default` 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. | ||
|
||
`Choice` is also added to the Amaranth prelude. | ||
|
||
In addition, the existing `Mux` expression is made valid on the left-hand side of an assignment, as if it was lowered as follows: | ||
|
||
```py | ||
def Mux(sel, val1, val0): | ||
return Choice(a, {0: val0}, default=val1) | ||
``` | ||
|
||
`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. | ||
|
||
## Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
The language gets slightly more complex. | ||
|
||
An extension for `m.Case` has been proposed that adds an optional guard condition to each case. It would be obviously desirable to mirror this functionality for `Choice`, but the syntax proposed here does not have space to add this. | ||
|
||
The design, as proposed, is almost but not quite capable of being used to implement `.word_select`: when an out-of-bounds offset is used on a signed value, the value should evaluate to the (replicated) sign bit when on right-hand side, but to nothing (ie. discard target) when on left-hand side. Since being usable as lowering target for `.word_select` is an explicit goal for `Choice`, this means some private API must be internally used to hack around this problem (either `Choice(..., _lhs_default=Cat())`, or adding some new `_DiscardOnLhs()` AST wrapper node type). | ||
|
||
The design is also not quite capable of replicating the current `ArrayProxy`, likewise due to the out-of-bounds case. However, since the behavior in that case is not documented and shouldn't be relied upon, the proposal is to simply change it. | ||
|
||
## Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
The core functionality is fairly obvious. However, the syntax is not. Other possibilities include: | ||
|
||
- `*args` (or perhaps iterable) of `(key, value)` tuples: | ||
|
||
```py | ||
Choices(sel, | ||
(1, a), | ||
(2, b), | ||
((3, 4), c), | ||
("11--", d), | ||
default=e | ||
) | ||
``` | ||
|
||
- *args of newly-defined `amaranth.hdl.Case` object (not to be confused with `m.Case`): | ||
|
||
```py | ||
Choices(sel, | ||
Case(1, a), | ||
Case(2, b), | ||
Case((3, 4), c), | ||
Case("11--, d), | ||
default=e, | ||
) | ||
``` | ||
|
||
This syntax, while wordy, has the desirable property of having non-awkward space for the proposed guard condition extension. | ||
|
||
## Prior art | ||
[prior-art]: #prior-art | ||
|
||
This feature is inspired by Rust `match` construct. | ||
|
||
## Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
A syntax for the cases needs to be picked. Several possibilities are included in this RFC. Preferably, the syntax should support optional guard conditions. | ||
|
||
The name is subject to bikeshed. An obvious alternative is `Match`. | ||
|
||
Should `Choice` try to preserve custom `ShapeCastable`s? We could define it to wrap the result in a `ShapeCastable` if the `shape()` of all inputs is the same `ShapeCastable`. Further, we could disallow having mismatched `ShapeCastable`s. | ||
|
||
## Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
Optional guard conditions could be added to `Choice` and `m.Switch` cases (like Rust's `if` guards on `match` branches). |