|
| 1 | +- Start Date: 2024-09-16 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#73](https://github.com/amaranth-lang/rfcs/pull/73) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#1511](https://github.com/amaranth-lang/amaranth/issues/1511) |
| 4 | + |
| 5 | +# Stricter connections |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Make it possible for `lib.wiring.connect()` to reject connections between ports with shapes that despite having equal widths are incompatible. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +The primary motivating use case is to be able to prevent connecting stream interfaces with incompatible payload shapes. |
| 16 | + |
| 17 | +## Guide-level explanation |
| 18 | +[guide-level-explanation]: #guide-level-explanation |
| 19 | + |
| 20 | +`lib.stream` defines a basic stream interface with `ready`, `valid` and `payload` members. |
| 21 | +Additional metadata signals can be added by making the `payload` shape an aggregate. |
| 22 | +This has the advantage that such streams can be passed through standard components like FIFOs without them having to know or care about these signals. |
| 23 | + |
| 24 | +Consider how we can make a byte-wide packetized stream by defining the `payload` shape as `StructLayout({"data": 8, "last": 1})`. |
| 25 | +The `data` field contains the data byte and the `last` field is a flag indicating that the current byte is the last in the packet. |
| 26 | + |
| 27 | +This is not the only common way to make a packetized stream. |
| 28 | +Instead of (or in addition to) the `last` flag, the payload could have a `first` flag, indicating that the current byte is the first in the packet. |
| 29 | +Both are valid options with different semantics for different usecases. |
| 30 | + |
| 31 | +The problem is that `lib.wiring.connect()` currently only checks that port members have matching widths, so a connection between a stream interface with `last` semantics and a stream interface with `first` semantics will not be rejected, despite being incompatible. |
| 32 | + |
| 33 | +Currently, `lib.data.View.eq()` does no checks on the passed value and immediately forwards to `self.as_value().eq()`, making this legal: |
| 34 | + |
| 35 | +```python |
| 36 | +>>> a = Signal(StructLayout({"data": 8, "last": 1})) |
| 37 | +>>> b = Signal(StructLayout({"data": 8, "first": 1})) |
| 38 | +>>> a.eq(b) |
| 39 | +(eq (sig a) (sig b)) |
| 40 | +``` |
| 41 | + |
| 42 | +This RFC proposes adding a check to `View.eq()` that would reject direct assignments from another aggregate data structure with a non-identical layout. |
| 43 | +If such an assignment is desired, the other aggregate data structure can be explicitly passed through `Value.cast()` first. |
| 44 | + |
| 45 | +Currently `lib.wiring.connect()` passes every signal through `Value.cast()` before assigning them. |
| 46 | +This results in a `ValueCastable`'s `.eq()` not being called, and thereby bypassing the check proposed above. |
| 47 | + |
| 48 | +This RFC proposes removing the `Value.cast()` so a `ValueCastable`'s `.eq()` will be called directly. |
| 49 | + |
| 50 | +This RFC proposes updating `lib.enum.EnumView` in the same manner, for the same reason, as `lib.data.View`. |
| 51 | + |
| 52 | +## Reference-level explanation |
| 53 | +[reference-level-explanation]: #reference-level-explanation |
| 54 | + |
| 55 | +Modify `lib.wiring.connect()` to not pass port members through `Value.cast()`, so that a `ValueCastable`'s `.eq()` will be called, allowing it to perform compatibility checks. |
| 56 | +- If a `ValueCastable` doesn't define `.eq()`, reject the assignment. |
| 57 | + |
| 58 | +Modify `lib.data.View.eq(other)` to add the following checks: |
| 59 | +- If `other` is a `ValueCastable`, do `Layout.cast(other.shape())` |
| 60 | + - If a valid `Layout` is returned, reject the assignment if it doesn't match `self.shape()`. |
| 61 | + - If `Layout.cast()` raises, reject the assignment. |
| 62 | +- Otherwise, proceed as normal. |
| 63 | + |
| 64 | +Modify `lib.enum.EnumView.eq(other)` to add the following checks: |
| 65 | +- If `other` is a `ValueCastable`, reject the assignment if `other.shape()` doesn't match `self.shape()`. |
| 66 | +- Otherwise, proceed as normal. |
| 67 | + |
| 68 | +Rejected assignments are a warning in Amaranth 0.6 and becomes a hard error in Amaranth 0.7. |
| 69 | + |
| 70 | +## Drawbacks |
| 71 | +[drawbacks]: #drawbacks |
| 72 | + |
| 73 | +- Increased language complexity. |
| 74 | + |
| 75 | +- This will add an implied requirement for a `ValueCastable` to implement `.eq()` to be usable with `lib.wiring`. Currently a `ValueCastable` is not required to implement `.eq()` at all. |
| 76 | + - We could fall back to `Value.cast().eq()` when `.eq()` is not defined. |
| 77 | + |
| 78 | +## Rationale and alternatives |
| 79 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 80 | + |
| 81 | +- Signals like `first` and `last` could be added as separate ports in the stream signature, preventing incompatible connections. |
| 82 | + - This was already discussed in [RFC 61](0061-minimal-streams.md) and decided against. |
| 83 | +- An explicit hook for checking compatibility between interfaces could be added, instead of relying on `.eq()`. |
| 84 | + - This RFC proposes picking the low-hanging fruits first, making use of already existing mechanisms. |
| 85 | + If this is not enough, another RFC can propose an explicit hook later. |
| 86 | + |
| 87 | +## Prior art |
| 88 | +[prior-art]: #prior-art |
| 89 | + |
| 90 | +[RFC 61](0061-minimal-streams.md) already discussed the need to eventually make `lib.wiring.connect()` stricter to prevent the connection of stream interfaces with incompatible payloads. |
| 91 | + |
| 92 | +## Unresolved questions |
| 93 | +[unresolved-questions]: #unresolved-questions |
| 94 | + |
| 95 | +None. |
| 96 | + |
| 97 | +## Future possibilities |
| 98 | +[future-possibilities]: #future-possibilities |
| 99 | + |
| 100 | +- A hook can still be added to allow a signature/interface to perform an explicit compatibility check, for cases where signatures have identical members but still have metadata indicating they are incompatible. |
| 101 | + - This hook could also allow overriding the existing checks, allowing connections where interfaces are compatible despite differences in members. |
0 commit comments