Skip to content

Commit b2c2204

Browse files
committed
RFC #73: Stricter connections.
1 parent 4352e0d commit b2c2204

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

text/0073-stricter-connections.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
- Start Date: (fill in with date at which the RFC is merged, YYYY-MM-DD)
2+
- RFC PR: [amaranth-lang/rfcs#73](https://github.com/amaranth-lang/rfcs/pull/73)
3+
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000)
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 `View` with a non-identical layout.
43+
If such an assignment is desired, the other `View` 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+
## Reference-level explanation
51+
[reference-level-explanation]: #reference-level-explanation
52+
53+
Modify `lib.data.View.eq(other)` to add the following checks:
54+
- If `other` is a `View`, reject the assignment if layouts are not identical.
55+
- Otherwise, if `other` is another `ValueCastable`, reject the assignment.
56+
- Otherwise, proceed as normal.
57+
58+
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.
59+
- If a `ValueCastable` doesn't define `.eq()`, reject the assignment.
60+
61+
Rejected assignments are a warning in Amaranth 0.6 and becomes a hard error in Amaranth 0.7.
62+
63+
## Drawbacks
64+
[drawbacks]: #drawbacks
65+
66+
- 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.
67+
- We could fall back to `Value.cast().eq()` when `.eq()` is not defined.
68+
69+
## Rationale and alternatives
70+
[rationale-and-alternatives]: #rationale-and-alternatives
71+
72+
- Signals like `first` and `last` could be added as separate ports in the stream signature, preventing incompatible connections.
73+
- This was already discussed in [RFC 61](0061-minimal-streams.md) and decided against.
74+
- An explicit hook for checking compatibility between interfaces could be added, instead of relying on `.eq()`.
75+
- This RFC proposes picking the low-hanging fruits first, making use of already existing mechanisms.
76+
If this is not enough, another RFC can propose an explicit hook later.
77+
78+
## Prior art
79+
[prior-art]: #prior-art
80+
81+
[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.
82+
83+
## Unresolved questions
84+
[unresolved-questions]: #unresolved-questions
85+
86+
None (yet).
87+
88+
## Future possibilities
89+
[future-possibilities]: #future-possibilities
90+
91+
- 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.
92+
- This hook could also allow overriding the existing checks, allowing connections where interfaces are compatible despite differences in members.

0 commit comments

Comments
 (0)