Skip to content

Commit

Permalink
Add RFC for Choice.
Browse files Browse the repository at this point in the history
  • Loading branch information
wanda-phi committed Mar 10, 2024
1 parent 0852d0e commit ad2a0a0
Showing 1 changed file with 181 additions and 0 deletions.
181 changes: 181 additions & 0 deletions text/0052-choice.md
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).

0 comments on commit ad2a0a0

Please sign in to comment.