Skip to content

Commit 2979ebf

Browse files
authored
RFC #73: Stricter connections.
2 parents 4352e0d + 6e0d5a6 commit 2979ebf

File tree

1 file changed

+101
-0
lines changed

1 file changed

+101
-0
lines changed

text/0073-stricter-connections.md

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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

Comments
 (0)