From a13452a907759c26c853afbc81d0b1d7ce2b7621 Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 16 Feb 2023 21:50:14 +0000 Subject: [PATCH 01/25] WIP RFC: Interface definition library. --- text/0001-aggregate-data-structures.md | 2 +- text/0002-interfaces.md | 473 +++++++++++++++++++++++++ 2 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 text/0002-interfaces.md diff --git a/text/0001-aggregate-data-structures.md b/text/0001-aggregate-data-structures.md index effc5c2..6ab1eb2 100644 --- a/text/0001-aggregate-data-structures.md +++ b/text/0001-aggregate-data-structures.md @@ -11,7 +11,7 @@ Add a rich set of standard library classes for accessing hierarchical aggregate data an idiomatic way, to fill one of the two major use cases of `Record` while avoiding its downsides. -See also #???. +See also [RFC #2](0002-interfaces.html). ## Motivation diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md new file mode 100644 index 0000000..e7e15d5 --- /dev/null +++ b/text/0002-interfaces.md @@ -0,0 +1,473 @@ +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- RFC PR: [amaranth-lang/rfcs#2](https://github.com/amaranth-lang/rfcs/pull/2) +- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) + +# Interface definition library RFC + + +## Summary + +Add standard ways of declaring that a component of a design conforms to a particular interface and connecting components with complementary interfaces together, to fill the other of the two major use cases of `Record` while avoiding its downsides. + +See also [RFC #1](0001-aggregate-data-structures.md). + + +## Motivation + +Digital designs are composed of densely packed components that communicate with each other using well-defined interfaces. Mechanisms to denote the boundary of a component, to ensure that a component complies to a specified interface, and to make reliable connections between components are essential. + +Currently, Amaranth provides none of these mechanisms. A component implemented in Amaranth, however well-defined conceptually, has no more external structure than a loose collection of `Signal`s assigned to its attributes; and whether any one of them is a part of the interface or the implementation is up to a guess. Even when an interface is described using `amaranth.hdl.rec.Layout`, such a description cannot be used to verify even the simplest aspects of compliance, such as presence of fields. Although building components by composing smaller components together is ubiquitous, `amaranth.hdl.rec.Layout` is not able to compose their interface with the same ease. Connecting components using `amaranth.hdl.rec.Record.connect` is difficult enough that it sees very little use. + +Originally, `Record` was aimed at solving many of these issues. However, it has multiple major drawbacks: + +1. `Record` attempts to do too much: it is both a mechanism for _controlling representation_ (including implicitly casting a record to a value) and a mechanism for _defining interfaces_ (specifying signal directions and facilitating connections between records). + + These mechanisms should be defined separately, since the only aspect they have in common is using a container class that consists of multiple named fields. Conflating the two mechanisms constraints the design space, making addressing the other drawbacks impossible, and the ill-defined scope encourages bugs in downstream code. + +2. `Record`'s model of signal directions is too complex. Because it attempts to model both aggregates with controlled representation and interfaces with defined directionality, every signal can have one of the three directions, the third option being non-directed. While this can be applied in a robust way--[FIRRTL](https://github.com/chipsalliance/firrtl-spec/releases/latest/download/spec.pdf) has only one aggregate type that it uses for both purposes--this gives rise to a large combination of features and requires handling many edge cases. + +3. `Record`'s model of signal directions is too limited. The two static directionalities it has are the confusingly named "fanout" and "fanin", which really mean "from initiator to target" and "from target to initiator". This is insufficient to describe common, straightforward interactions such as two components exchanging streams of data across pairs of identical, complementary endpoints. + +4. `Record`s are hard to customize. Records create and hold their signals, only providing the caller with an ability to place caller-created signals into individual fields. Signals often need adjustments: primarily setting a reset value or adding a decoder, but sometimes adding attributes or renaming. These adjustments must be performed at the record creation site, which is burdensome. + +5. `Record` fields can be (apart from sub-records) only plain signals. In many cases, an interface between components carries structured data rather than opaque bit vectors. It is not possible to define inner structure for record fields other than through a sub-record, and using a sub-record for this means that an application-specific endpoint that defines such structure cannot be connected to a generic endpoint that does not. + +6. `Record`s are hard to compose. The natural way to define a record is to call the `Record` constructor with a layout, but this creates the entire layout hierarchy unless parts of it are replaced; and it requires having the layout of the result in advance. + +7. `Record.connect` determines the direction of data flow that it will create by the relative position of the interfaces being connected, with `x.connect(y)` and `y.connect(x)` having the oposite polarity of assignments. However, the direction of data flow is defined by the component that exposes the interface. Thus, every call of `Record.connect` can be done in one of the two very similar ways, one of which is always wrong. + +8. `Record.connect` uses wired-OR to gather the "fanin" signals, a feature that exists so that it could be used to connect e.g. Wishbone endpoints together without additional gateware. The assumption that the response signals of inactive endpoints will remain all-zero is, generally, unsound. + +9. `Record.connect` manages connections between interfaces with optional signals at the call site using an include/exclude mechanism. However, the semantics of the non-implemented optional signals are a property of the interface, not the connection. + +10. `Record` and `rec.Layout` are often used as base classes. The `Record.like` facility, frequently used because of the poor ergonomics of `rec.Layout`, loses this information and returns an instance of the base class; `rec.Layout` does the same when indexed. As a result, there is little value in defining methods and attributes on the subclass, and `Record` subclasses are little more than a callable computing a layout. + +11. Due to the limitations of `Record`, one might define a plain Python class that exposes compatible attributes. An instance of such a class cannot be compared to a known `rec.Layout` nor can it be embedded in another `Record`. + +12. `Record` is value-castable and implements the `.eq()` protocol. Although useful when all fields are non-directional, using `.eq()` instead of `.connect()` when connecting directional interfaces is, generally, unsound. It also reserves commonly used names such as `any`, `all`, and `matches`, and implements arithmetic operations that are rarely if ever used on field containers. + +13. `rec.Layout`'s DSL is very amorphous. It passes around variable length tuples. The second element of these tuples (the shape) can be another `rec.Layout`, which is neither a shape nor a shape-castable object. + +Since these drawbacks are entrenched in the public API and make `Record` nearly useless for defining interfaces, a new mechanism must replace it. + + +## Outline of the design space + +Although some HDLs and IRs (Migen, Chisel, FIRRTL, ...) choose to use the same basic aggregate data type to represent _structured data_ and _directional interfaces_, these mechanisms are in direct conflict. Complex forms of structured data, such as unions, are incompatible with associating directionality independently with every leaf member; and the non-directional nature of stored data requires complicated and error-prone rules when it can become a part of a directional connection. + +Amaranth, instead, opts to include two superficially similar mechanisms for defining and accessing hierarchical aggregate data: `amaranth.lib.data` (RFC #1) and `amaranth.lib.component` (this RFC). `amaranth.lib.data` provides _data views_ that reinterpret bit containers as complex aggregates, and entirely avoids directionality. `amaranth.lib.component` provides _signatures_ that give a concrete shape to signals at component boundaries, and always treats them as directional. + +When connections are made strictly between an output and a correspondingly named input, interfaces gain a dualistic nature: every connection is made between two interfaces whose port directions are the inverse of each other, and which are identical otherwise. To describe interfaces without repeating oneself, then, one has to pick an arbitrarily preferred directionality (and stick with it). Many interfaces are asymmetric, with data flowing from a source to a sink, or transactions issued from an initiator to a target. Amaranth picks the _source_ or _initiator_ perspective; an interface, examined in isolation, defines as outputs the signals that would be outputs of an initiator (and inputs of a target). Then, when an interface with true (non-flipped) directionality describes a component's output, the same interface with inverse (flipped) directionality symmetrically describes an input. + +To eliminate the major usability issues with `Record.connect`, the interface connection mechanism assigns no precedence to interfaces and has no effect on signal directionality; whether a signal is an input or an output depends only on the interface itself. A connection is only made from an output to a matching input, and any other combination is rejected with a diagnostic. This way, connecting a pair of interfaces always leads to the same outcome, regardless of their order. + +The choice to always treat interface signals as directional and to make their directionality dependent only on the interface itself leaves only one aspect of the design open: when and how interface directionality is flipped. The decisions that determine it affect both ergonomics and soundness. `Record.connect` in effect gives the programmer an option to flip directionality even when it would create an illegal connection. Conversely, `rec.Layout` provides no such affordance, even though it is necessary for composing components. + +To facilitate composing components, the interface's directionality is flipped when it is used as an input, whether a top-level input of a component, or as a constituent of a larger interface. This way, the existing mechanism of annotating the directionality of an interface signal or a module port transparently handles interface composition. + + +## Guide-level explanation + +Amaranth designs are made out of components (Python classes implementing `Elaboratable`) whose attributes include signals. These signals have directions: "in" signals are sampled by the component, while "out" signals are driven by it, or left at their reset value, and are provided to be sampled by other components. + +At the moment, these directions are completely informal, and described in the documentation and/or in the signal name as an `i_` or `o_` prefix (to make it clearer what the direction is at the point of use, or to disambiguate the ports that would otherwise have identical name): + +```python +class SequenceSource(Elaboratable): + """ + Ports + ----- + data : Signal(width), out + ready : Signal(1), in + valid : Signal(1), out + """ + def __init__(self, width=16): + self.data = Signal(width) + self.ready = Signal() + self.valid = Signal(reset=1) + + def elaborate(self, platform): + m = Module() + with m.If(self.ready): + m.d.sync += self.data.eq(self.data + 1) + return m +``` + +The `SequenceSource` component is implemented as a simple counter producing values for an *output stream* that is connected to some other component consuming them. On each clock tick, if the consumer is *ready*, it samples the *data* (the counter value), and simultaneously with that, the producer advances the data to the next item (the incremented value). Since there is always a next item available and it is ready for consumption on the next clock cycle, the stream always contains *valid* results. + +> **Note** +> It is not, in general, possible to infer the directions of the signals from the implementation—here, `ready` and `valid` have different directions and different intended uses, but they look similar to the Amaranth implementation since they are both undriven in the component. + +This RFC proposes a way of describing signal directions that can be applied to any Python object. In addition to elaboratables, it includes Python objects that are used to group together signals with a similar purpose, such as those that are parts of a bus. + +To describe signal directions, only a single addition is needed: the `signature` property: + +```python +class SequenceSource(Elaboratable): + ... + + signature = Signature({ + "data": Out[16], + "ready": In[1], + "valid": Out[1] + }) +``` + +Consider another component that is consuming these values: + +```python +class NumberSink(Elaboratable): + ... + + def elaborate(self): + m = Module() + processing = Signal() + m.d.comb += self.ready.eq(~processing) + with m.If(self.valid & ~processing): + m.d.sync += processing.eq(1) + with m.Elif(processing): + ... # process it somehow + + signature = Signature({ + "data": In[16], + "ready": Out[1], + "valid": In[1] + }) +``` + +Currently, the only way (given the tools provided by the language and the standard library) to connect the *output stream* of the `SequenceSource` to the *input stream* of the `NumberSink` is to do this signal-wise: + +```python +m = Module() +m.submodules.source = source = SequenceSource() +m.submodules.sink = sink = NumberSink() +m.d.comb += [ + sink.data.eq(source.data), + source.ready.eq(sink.ready), + sink.valid.eq(source.valid) +] +``` + +This is tedious, verbose, and error-prone. It is possible to define an application-specific function abstracting this operation, and many applications do, but something this universal should be defined on the language level. + +This RFC introduces a way to describe interfaces (collections of directional signals; more on this later) and a single operation: *connecting*. The code above now transforms into: + +```python +from amaranth.lib import component + +m = Module() +m.submodules.source = source = SequenceSource() +m.submodules.sink = sink = NumberSink() +component.connect(m, sink, source) +``` + +The order of arguments to `component.connect` does not matter as the directionality is defined by the components themselves. It could just as well be written as: + +```python +component.connect(m, source, sink) +``` + +However, this approach still has flaws. Most importantly, the signature for `SequenceSource` and `NumberSink` is written twice, but their `signature` is exactly the same except that the direction is flipped: `In` members become `Out`, and vice versa. To avoid error-prone repetition here, the signature can be defined once: + +```python +Stream16BitSignature = Signature({ + "data": Out[16], + "ready": In[1], + "valid": Out[1] +}) +``` + +and then used twice, for both the source and the sink: + +```python +class SequenceSource(Elaboratable): + ... + + signature = Stream16BitSignature + +class NumberSink(Elaboratable): + ... + + signature = Stream16BitSignature.flip() +``` + +The `Signature.flip()` method returns a _flipped signature object_: a signature object whose members have inverse direction but which is otherwise identical. + +Since this approach has reusable signatures defined with a specific direction, it is necessary to make an arbitrary choice: pick the kind of object whose signature will use the non-flipped directions. This RFC picks the object that is the _source of data_ (for stream-like interfaces), the _transaction initiator_ (for bus-like interfaces), and so on to use non-flipped directions by convention. + +Although some duplication was eliminated, some more remains: currently, it is necessary to define a stream signature for every kind of stream (a stream of 16-bit values, a stream of RGB colors, and so on). It is possible to define a reusable stream signature by inheriting from the `Signature` class: + +```python +class StreamSignature(Signature): + def __init__(self, payload_shape): + return super().__init__({ + "payload": Out[payload_shape], + "ready": In[1], + "valid": Out[1] + }) +``` + +The elaboratables above can then be defined as: + +```python +class SequenceSource(Elaboratable): + ... + + signature = StreamSignature(16) + +class NumberSink(Elaboratable): + ... + + signature = StreamSignature(16).flip() +``` + +Usually, elaboratables have more than one interface. For example, a very simple DSP block could sink a stream of signed numbers, take their absolute value, and source a stream of unsigned numbers. It would then have a pair of `ready`, `valid`, and `payload` signals each: one for the input steam, and another for the output stream. + +To handle this case, signature's members can be signatures themselves. These members also have directionality; an `Out` signature leaves the directionality of its members unchanged, while an `In` signature flips it. The signature method of the processing block could be defined as: + +```python +class AbsoluteProcessor(Elaboratable): + ... + + signature = Signature({ + "i": In[StreamSignature(signed(16))], + "o": Out[StreamSignature(unsigned(16))] + }) +``` + +To be compatible with this signature, an `AbsoluteProcessor` instance must have an `i` attribute compatible with a `StreamSignature(signed(16)).flip()`, and an `o` attribute compatible with a `StreamSignature(unsigned(16))`. These could be defined manually: + +```python +class AbsoluteProcessor(Elaboratable): + def __init__(self): + self.i = object() + self.i.payload = Signal(signed(16)) + self.i.ready = Signal() + self.i.valid = Signal() + self.i.signature = StreamSignature(signed(16)).flip() + + self.o = object() + self.o.payload = Signal(unsigned(16)) + self.o.ready = Signal() + self.o.valid = Signal() + self.o.signature = StreamSignature(unsigned(16)) + + ... +``` + +Once more, to reduce error-prone repetition, the `Signature` class offers a way to define objects just like the ones created above, making the complete definition be: + +```python +class AbsoluteProcessor(Elaboratable): + def __init__(self): + self.i = StreamSignature(signed(16)).flip().apply() + self.o = StreamSignature(unsigned(16)).apply() + + signature = Signature({ + "i": In[StreamSignature(signed(16))], + "o": Out[StreamSignature(unsigned(16))] + }) + + ... +``` + +However, since the interface of `AbsoluteProcessor` as a whole can itself be described as a signature, it is possible to further shorten it by passing `self` to the `apply` method, in which case it will update attributes of the provided object instead of creating a new one: + +```python +class AbsoluteProcessor(Elaboratable): + signature = Signature({ + "i": In[StreamSignature(signed(16))], + "o": Out[StreamSignature(unsigned(16))] + }) + + def __init__(self): + self.signature.apply(self) + + def elaborate(self): + m = Module() + with m.If(self.i.payload > 0): + m.d.comb += self.o.payload.eq(self.i.payload) + with m.Else(): + # Does not overflow, since -(-32768) [least signed(16)] is less + # than 65536 [greatest unsigned(16)]. + m.d.comb += self.o.payload.eq(-self.i.payload) + return m +``` + +`Signature` subclasses can also override the `apply` method to add functionality not present in the base class. For example, a signature for a bus such as Wishbone or AXI could return an instance of a class rather than a simple `object()`, and include attributes indicating which optional features of the bus are enabled. + + +## Reference-level explanation + +This RFC proposes a number of library additions: + +* Adding classes that describe a hierarchy of Amaranth objects (an elaboratable object and the objects containing its interface signals) and ease instantiating such hierarchies. +* Adding a function that connects such hierarchies to each other. + +It also introduces a number of technical terms: + +* A _component_ is an Amaranth elaboratable object. +* An _interface_ (a concept) is a shared boundary across which several Amaranth components exchange data. It is comprised of a set of signals and the invairants that govern their use. +* An _interface object_ (an implementation of the concept) a Python object that includes: + 1. attributes whose value is an Amaranth value-castable, or another interface; + 2. a `signature` attribute whose value is a _signature_ that is _compatible_ with this object; + 3. a description of the invariants applying to its use (in form of documentation, testbenches, formal tests, etc.). +* A _signature_ is a `Signature` instance describing requirements applicable to a hierarchy of interace objects. +* A _signature member_ is a `Member` instance describing requirements applicable to a single attribute of an interface object. Two kinds of signature members exist: port members (requiring the value of the attribute to be a `Signal`), and interface members (requiring the value of the attribute to be another interface object). +* A signature is _compatible_ with an object (therefore making it an interface object) if every member of the signature object corresponds to an attribute of the object whose value fits the requirements. + +A single elaboratable object will often have several interfaces; e.g. a peripheral can have a CSR and/or Wishbone bus interface, and a pin interface. However, the elaboratable object itself can be an interface object as well, which makes it easy to convert it to Verilog and use standalone since its signature defines the ports the Verilog module needs to have. + + +### Interface description + +Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, and two classes, `amaranth.lib.component.Member` and `amaranth.lib.component.Signature`: + +* `Flow` is an enumeration with two values, `In` and `Out`. + + * `Flow.__call__(arg, **kwargs)` forwards to `Member(self, arg, **kwargs)`. + * Thus, `Out(unsigned(16), reset=0x1234)` is a shorthand for `Member(Flow.Out, unsigned(16), reset=0x1234)`. + * `flow.flip()` flips the value from `In` to `Out` and back. + +* A `Member(flow, ...)` object describes a part of an interface. It is immutable. + + * A `Member(flow, shape_castable, reset=reset_value)` object describes a port with the given shape and flow direction. The returned `Member` object has: + * the `.flow` property be `flow`; + * the `.is_port` property be `True`; + * the `.is_signature` property be `False`; + * the `.shape` property be `shape_castable`; + * the `.reset` property be `reset_value`; + * the `.signature` property raise `TypeError`; + * the `.dimensions` property be `()`. + * A `Member(flow, signature)` object describes a constituent interface with the given flow direction. If `flow` is `Flow.In`, then the actual flow of every port recursively described by `signature` is the reverse of the stated direction. The returned `Member` object has: + * the `.flow` property be `flow`; + * the `.is_port` property be `False`; + * the `.is_signature` property be `True`; + * the `.shape` property raise `TypeError`; + * the `.reset` property raise `TypeError`; + * the `.signature` property return `signature`; + * the `.dimensions` property be `()`. + * `member.array(*dimensions)` returns a new `Member` object whose `.dimensions` property is `dimensions`, which is any amount of non-negative numbers, and all other properties are the same as those of `member`. + * `member.flip()` returns a new `Member` object whose `.flow` property is `~member.flow`, and all other properties are the same as those of `member`. + +* A `Signature(...)` object describes an interface comprised of named members: ports and nested interfaces (which themselves are described using signature objects). + + The `Signature` class can be derived from. Instances of `Signature` itself are termed _anonymous signatures_, and instances of derived classes are _named signatures_. + + * A `Signature({"name": Member(...)})` object can be constructed from a name to member mapping. + * `signature.members` is a mutable mapping that can be used to alter the description of a non-frozen signature. + * `signature.members += {...}` adds members from the given mapping to `signature.members` if the names being added are not already used. Raises `NameError` otherwise. + * `signature.freeze()` prevents any further modifications of `signature.members`, enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. + * `signature.__eq__()` compares: + * anonymous signatures, which are equal when the members and their names compare equal; + * named signatures, which are equal only to themselves (the same signature object), unless overridden in a derived class. + * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. + * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. + * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides `__iter__` and `__getitem__` to flip the direction of members, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. + * `signature.compatible(object)` checks whether an arbitrary Python object is compatible with this signature. To be compatible with a signature: + - for every member of the signature, the object must have a corresponding attribute + - if the member is a port, the attribute value must be a value-castable such that `Value.cast(object.attr)` method returns a `Signal` or a `Const` that has the same width and signedness, and for signals, is not reset-less and has the same reset value as the member + - a warning may be emitted if the `.shape` of the member and the `.shape()` of `object.attr` are not equal + - if the member is an interface, the attribute value must be compatible with the signature of the member + - if the member's `dimensions` are `(p, q, ...)`, the requirements below hold instead for every result of indexing the attribute value with `[i][j]...` where `i in range(p)`, `j in range(q)`, ... + * `signature.create_members()` freezes this signature and creates a dictionary of members from it. This is a helper method that is essentially the part of `create()` that subclasses are unlikely to need to override. For every member of the signature, the dictionary contains a value equal to: + * If the member is a port, `Signal(member.shape, reset=member.reset)`. + * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. + * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `object()` and replaces its dictionary with the result of `signature.create_members()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. + + +### Interface connection + +Interface objects may be connected to each other using the `amaranth.lib.component.connect(m, first, *rest)` free function. + +This function connects interface objects that satisfy the following requirements: +* For every port member with a given `path` and `first_shape` in `first`, there is a port member with the same `path` in each of `rest` with shape `rest_shape`, where `Shape.cast(first_shape) == Shape.cast(rest_shape)`, and there are no other members in any of `rest`. In other words, the width and signedness of all of port members must be equal, but the shape-castable objects specifying the width and signedness do not have to be. + * It is expected that any mismatch in signatures will be resolved (through wrappers, mutating signature members, or otherwise) before interfaces are being connected. +* Either: + 1. There is exactly one interface object in `rest`, and for every pair of members `first_member` and `rest_member`, `first_member.flow == ~rest_member.flow`. + 2. There is any amount of interfaces in `rest`, and for every pair of members `first_member` and `rest_member`, `first_member.flow == Out` and `rest_member.flow == In`. + +In both of the cases (1) and (2), the function, for each pair of an input port and an output of port with the same path, the function connects these as follows: +```python +m.d.comb += output_port.eq(input_port) +``` + + +### Interface forwarding + +In some cases, an outer elaboratable object creates an inner elaboratable object and _forwards_ an interface of the inner object like this: + +```python +class Outer(Elaboratable): + def __init__(self): + self.inner = Inner() + + Signature({ + "bus": BusSignature() + }).apply(self) + + def elaborate(self, platform): + m = Module() + m.d.comb += [ + self.inner.bus.addr.eq(self.bus.addr), + self.inner.bus.w_data.eq(self.bus.w_data), + self.bus.r_data.eq(self.inner.bus.r_data), + # ... + ] + return m + + +class Inner(Elaboratable): + def __init__(self): + Signature({ + "bus": BusSignature() + }).apply(self) + + ... +``` + +In this case, `amaranth.lib.component.connect(...)` won't help, since an output needs to be connected to an output, and an input to an input. + +An additional function `amaranth.lib.component.forward(obj)` is added to assist in this case. It returns a proxy object `obj_forward` where `obj_forward.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: + +```python +class Outer(Elaboratable): + def elaborate(self, platform): + m = Module() + component.connect(m, component.forward(self.bus), self.inner.bus) + return m +``` + + +### Component definition + +This RFC in effect introduces a particular kind of elaboratable object: one that has a signature. While connecting an elaboratable as a whole (as opposed to its sub-interfaces) will rarely, if ever, happen, it is still convenient to have an elaboratable define its signature, for three reasons: +1. It is a declaration of intent, separating the signals that are purposefully a part of its interface from ones that just happen to be assigned to attributes, and stating their direction; +2. It simplifies and standardizes assignment of the interface attributes, making the `signature` property the single source of truth for the module's interface; +3. It makes it easy to convert a single standalone elaboratable to Verilog. + +To this end, a class `amaranth.lib.component.Component` is introduced: +* `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.create_members()`. + * TBD: what to do in case of a name conflict? abort (then to override, `super().__init__` call will be in the beginning) or ignore (then to override, `super().__init__` call will be in the end) +* `Component.signature` collects PEP 526 variable annotations in the class, if any, and returns a signature object constructed from these, or raises an error otherwise. The signature object is created per-instance, not per-class, so that it can be safely mutated if this is a part of the workflow. + + +## Alternatives and rationale + +- Do nothing. `Record` will continue to be used alongside the continued proliferation of ad-hoc implementations of similar functionality, and continue to impair the use of Amaranth components together. + +- Replace the `interface.lib.component.connect` free function with a function `amaranth.hdl.dsl.Module.connect`. + * It is not a function on `amaranth.hdl.dsl.Module` to avoid privileging the standard interface library over any other library that may be written downstream. At the moment nothing in `amaranth.lib` is special in any way other than its name, and preserving this is valuable to the author. + + +## Naming questions + +- Should `Signature` be called `Interface`? + - The object returned by `Signature.apply()` is an interface, not the signature itself +- Should `Signature.compatible` be named something else, like `Signature.is_implemented`? +- Should `component.forward` be named something else, like `component.evert` or `component.flip`? + + +## Future work + +* One-to-many connections between interfaces are currently provided only with a fan-out topology: a single interface with output members only can be connected with multiple interfaces with input members only. This avoids the question of what to do with an input that must be driven by multiple outputs. The interface library could be enriched by adding a small amount of fixed fan-in topologies, e.g. wired-OR and wired-AND, specified as a `Member()` constructor parameter that must match between all of the respective members. From a58338691105e011eb8f708ec428c93e1d651788 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 14:44:29 +0000 Subject: [PATCH 02/25] Update outdated code examples and add one with type annotations --- text/0002-interfaces.md | 95 ++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index e7e15d5..0bc9fb1 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -107,9 +107,9 @@ class SequenceSource(Elaboratable): ... signature = Signature({ - "data": Out[16], - "ready": In[1], - "valid": Out[1] + "data": Out(16), + "ready": In(1), + "valid": Out(1) }) ``` @@ -129,9 +129,9 @@ class NumberSink(Elaboratable): ... # process it somehow signature = Signature({ - "data": In[16], - "ready": Out[1], - "valid": In[1] + "data": In(16), + "ready": Out(1), + "valid": In(1) }) ``` @@ -171,9 +171,9 @@ However, this approach still has flaws. Most importantly, the signature for `Seq ```python Stream16BitSignature = Signature({ - "data": Out[16], - "ready": In[1], - "valid": Out[1] + "data": Out(16), + "ready": In(1), + "valid": Out(1) }) ``` @@ -201,9 +201,9 @@ Although some duplication was eliminated, some more remains: currently, it is ne class StreamSignature(Signature): def __init__(self, payload_shape): return super().__init__({ - "payload": Out[payload_shape], - "ready": In[1], - "valid": Out[1] + "payload": Out(payload_shape), + "ready": In(1), + "valid": Out(1) }) ``` @@ -230,8 +230,8 @@ class AbsoluteProcessor(Elaboratable): ... signature = Signature({ - "i": In[StreamSignature(signed(16))], - "o": Out[StreamSignature(unsigned(16))] + "i": In(StreamSignature(signed(16))), + "o": Out(StreamSignature(unsigned(16))) }) ``` @@ -260,29 +260,28 @@ Once more, to reduce error-prone repetition, the `Signature` class offers a way ```python class AbsoluteProcessor(Elaboratable): def __init__(self): - self.i = StreamSignature(signed(16)).flip().apply() - self.o = StreamSignature(unsigned(16)).apply() + self.i = StreamSignature(signed(16)).flip().create() + self.o = StreamSignature(unsigned(16)).create() signature = Signature({ - "i": In[StreamSignature(signed(16))], - "o": Out[StreamSignature(unsigned(16))] + "i": In(StreamSignature(signed(16))), + "o": Out(StreamSignature(unsigned(16))) }) ... ``` -However, since the interface of `AbsoluteProcessor` as a whole can itself be described as a signature, it is possible to further shorten it by passing `self` to the `apply` method, in which case it will update attributes of the provided object instead of creating a new one: +`Signature` subclasses can also override the `create` method to add functionality not present in the base class. For example, a signature for a bus such as Wishbone or AXI could return an instance of a class rather than a simple `object()`, and include attributes indicating which optional features of the bus are enabled. + +However, since the interface of `AbsoluteProcessor` as a whole can itself be described as a signature, it is possible to further shorten it by deriving from `component.Component` instead of `Elaboratable`, in which case the attributes will be filled in from the signature automatically: ```python -class AbsoluteProcessor(Elaboratable): +class AbsoluteProcessor(component.Component): signature = Signature({ - "i": In[StreamSignature(signed(16))], - "o": Out[StreamSignature(unsigned(16))] + "i": In(StreamSignature(signed(16))), + "o": Out(StreamSignature(unsigned(16))) }) - def __init__(self): - self.signature.apply(self) - def elaborate(self): m = Module() with m.If(self.i.payload > 0): @@ -294,7 +293,23 @@ class AbsoluteProcessor(Elaboratable): return m ``` -`Signature` subclasses can also override the `apply` method to add functionality not present in the base class. For example, a signature for a bus such as Wishbone or AXI could return an instance of a class rather than a simple `object()`, and include attributes indicating which optional features of the bus are enabled. +Python variable annotations can also be used in cases like the above, where the signature is the same for every instance of the class (i.e. the component is not parameterized during creation): + +```python +class AbsoluteProcessor(component.Component): + i: In(StreamSignature(signed(16))) + o: Out(StreamSignature(unsigned(16))) + + def elaborate(self): + m = Module() + with m.If(self.i.payload > 0): + m.d.comb += self.o.payload.eq(self.i.payload) + with m.Else(): + # Does not overflow, since -(-32768) [least signed(16)] is less + # than 65536 [greatest unsigned(16)]. + m.d.comb += self.o.payload.eq(-self.i.payload) + return m +``` ## Reference-level explanation @@ -398,13 +413,13 @@ m.d.comb += output_port.eq(input_port) In some cases, an outer elaboratable object creates an inner elaboratable object and _forwards_ an interface of the inner object like this: ```python -class Outer(Elaboratable): +class Outer(Component): + bus: BusSignature() + def __init__(self): - self.inner = Inner() + super().__init__() - Signature({ - "bus": BusSignature() - }).apply(self) + self.inner = Inner() def elaborate(self, platform): m = Module() @@ -417,11 +432,8 @@ class Outer(Elaboratable): return m -class Inner(Elaboratable): - def __init__(self): - Signature({ - "bus": BusSignature() - }).apply(self) +class Inner(Component): + bus: BusSignature() ... ``` @@ -431,7 +443,14 @@ In this case, `amaranth.lib.component.connect(...)` won't help, since an output An additional function `amaranth.lib.component.forward(obj)` is added to assist in this case. It returns a proxy object `obj_forward` where `obj_forward.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: ```python -class Outer(Elaboratable): +class Outer(Component): + bus: BusSignature() + + def __init__(self): + super().__init__() + + self.inner = Inner() + def elaborate(self, platform): m = Module() component.connect(m, component.forward(self.bus), self.inner.bus) @@ -463,7 +482,7 @@ To this end, a class `amaranth.lib.component.Component` is introduced: ## Naming questions - Should `Signature` be called `Interface`? - - The object returned by `Signature.apply()` is an interface, not the signature itself + - The object returned by `Signature.create()` is an interface, not the signature itself - Should `Signature.compatible` be named something else, like `Signature.is_implemented`? - Should `component.forward` be named something else, like `component.evert` or `component.flip`? From 94a4d646ab787811e22fe374d71fd592bf8c899c Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 14:51:23 +0000 Subject: [PATCH 03/25] More fixes to code examples --- text/0002-interfaces.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 0bc9fb1..432a571 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -103,6 +103,9 @@ This RFC proposes a way of describing signal directions that can be applied to a To describe signal directions, only a single addition is needed: the `signature` property: ```python +from amaranth.lib.component import Signature, In, Out + + class SequenceSource(Elaboratable): ... @@ -153,18 +156,19 @@ This is tedious, verbose, and error-prone. It is possible to define an applicati This RFC introduces a way to describe interfaces (collections of directional signals; more on this later) and a single operation: *connecting*. The code above now transforms into: ```python -from amaranth.lib import component +from amaranth.lib.component import connect + m = Module() m.submodules.source = source = SequenceSource() m.submodules.sink = sink = NumberSink() -component.connect(m, sink, source) +connect(m, sink, source) ``` -The order of arguments to `component.connect` does not matter as the directionality is defined by the components themselves. It could just as well be written as: +The order of arguments to `connect` does not matter as the directionality is defined by the components themselves. It could just as well be written as: ```python -component.connect(m, source, sink) +connect(m, source, sink) ``` However, this approach still has flaws. Most importantly, the signature for `SequenceSource` and `NumberSink` is written twice, but their `signature` is exactly the same except that the direction is flipped: `In` members become `Out`, and vice versa. To avoid error-prone repetition here, the signature can be defined once: @@ -276,7 +280,10 @@ class AbsoluteProcessor(Elaboratable): However, since the interface of `AbsoluteProcessor` as a whole can itself be described as a signature, it is possible to further shorten it by deriving from `component.Component` instead of `Elaboratable`, in which case the attributes will be filled in from the signature automatically: ```python -class AbsoluteProcessor(component.Component): +from amaranth.lib.component import Component + + +class AbsoluteProcessor(Component): signature = Signature({ "i": In(StreamSignature(signed(16))), "o": Out(StreamSignature(unsigned(16))) @@ -296,7 +303,7 @@ class AbsoluteProcessor(component.Component): Python variable annotations can also be used in cases like the above, where the signature is the same for every instance of the class (i.e. the component is not parameterized during creation): ```python -class AbsoluteProcessor(component.Component): +class AbsoluteProcessor(Component): i: In(StreamSignature(signed(16))) o: Out(StreamSignature(unsigned(16))) @@ -311,6 +318,12 @@ class AbsoluteProcessor(component.Component): return m ``` +All of the import statements in the code examples above can be replaced with: + +```python +from amaranth.lib.component import * +``` + ## Reference-level explanation From 0ab3d435186316e27fffa83dee44be9aed722074 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 15:13:07 +0000 Subject: [PATCH 04/25] Update naming questions --- text/0002-interfaces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 432a571..cefaf4c 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -497,7 +497,7 @@ To this end, a class `amaranth.lib.component.Component` is introduced: - Should `Signature` be called `Interface`? - The object returned by `Signature.create()` is an interface, not the signature itself - Should `Signature.compatible` be named something else, like `Signature.is_implemented`? -- Should `component.forward` be named something else, like `component.evert` or `component.flip`? +- Should `component.forward` be named something else, like `component.forwarded` or `component.forwarding` or `component.evert` or `component.flip`? ## Future work From 58a2820eee600d50a7d5a726df7843824f18b5f6 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 18:49:46 +0000 Subject: [PATCH 05/25] `Signature.create_members()` -> `Signature.members.create()` --- text/0002-interfaces.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index cefaf4c..b921743 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -385,23 +385,23 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an * A `Signature({"name": Member(...)})` object can be constructed from a name to member mapping. * `signature.members` is a mutable mapping that can be used to alter the description of a non-frozen signature. * `signature.members += {...}` adds members from the given mapping to `signature.members` if the names being added are not already used. Raises `NameError` otherwise. - * `signature.freeze()` prevents any further modifications of `signature.members`, enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. + * `signature.freeze()` (or `signature.members.freeze()`) prevents any further modifications of `signature.members`, enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. * `signature.__eq__()` compares: * anonymous signatures, which are equal when the members and their names compare equal; * named signatures, which are equal only to themselves (the same signature object), unless overridden in a derived class. * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. - * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides `__iter__` and `__getitem__` to flip the direction of members, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. + * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the direction is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. * `signature.compatible(object)` checks whether an arbitrary Python object is compatible with this signature. To be compatible with a signature: - for every member of the signature, the object must have a corresponding attribute - if the member is a port, the attribute value must be a value-castable such that `Value.cast(object.attr)` method returns a `Signal` or a `Const` that has the same width and signedness, and for signals, is not reset-less and has the same reset value as the member - a warning may be emitted if the `.shape` of the member and the `.shape()` of `object.attr` are not equal - if the member is an interface, the attribute value must be compatible with the signature of the member - if the member's `dimensions` are `(p, q, ...)`, the requirements below hold instead for every result of indexing the attribute value with `[i][j]...` where `i in range(p)`, `j in range(q)`, ... - * `signature.create_members()` freezes this signature and creates a dictionary of members from it. This is a helper method that is essentially the part of `create()` that subclasses are unlikely to need to override. For every member of the signature, the dictionary contains a value equal to: + * `signature.members.create()` creates a dictionary of members from it. This is a helper method that is essentially the part of `create()` that subclasses are unlikely to need to override. For every member of the signature, the dictionary contains a value equal to: * If the member is a port, `Signal(member.shape, reset=member.reset)`. * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. - * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `object()` and replaces its dictionary with the result of `signature.create_members()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. + * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `object()` and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. ### Interface connection @@ -479,7 +479,7 @@ This RFC in effect introduces a particular kind of elaboratable object: one that 3. It makes it easy to convert a single standalone elaboratable to Verilog. To this end, a class `amaranth.lib.component.Component` is introduced: -* `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.create_members()`. +* `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.members.create()`. * TBD: what to do in case of a name conflict? abort (then to override, `super().__init__` call will be in the beginning) or ignore (then to override, `super().__init__` call will be in the end) * `Component.signature` collects PEP 526 variable annotations in the class, if any, and returns a signature object constructed from these, or raises an error otherwise. The signature object is created per-instance, not per-class, so that it can be safely mutated if this is a part of the workflow. From 27c4e2b248c8a069afcb3f94399b84db419cf687 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 18:50:05 +0000 Subject: [PATCH 06/25] Remove `Signature.__eq__()` since it's hard to implement when flipping exists --- text/0002-interfaces.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index b921743..d71ea07 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -386,9 +386,6 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an * `signature.members` is a mutable mapping that can be used to alter the description of a non-frozen signature. * `signature.members += {...}` adds members from the given mapping to `signature.members` if the names being added are not already used. Raises `NameError` otherwise. * `signature.freeze()` (or `signature.members.freeze()`) prevents any further modifications of `signature.members`, enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. - * `signature.__eq__()` compares: - * anonymous signatures, which are equal when the members and their names compare equal; - * named signatures, which are equal only to themselves (the same signature object), unless overridden in a derived class. * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the direction is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. From 4231d4e36922caa94e01185b83580c3755648724 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 20:08:10 +0000 Subject: [PATCH 07/25] `Component.__init__` to abort on name conflict --- text/0002-interfaces.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index d71ea07..87ad4d0 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -476,8 +476,7 @@ This RFC in effect introduces a particular kind of elaboratable object: one that 3. It makes it easy to convert a single standalone elaboratable to Verilog. To this end, a class `amaranth.lib.component.Component` is introduced: -* `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.members.create()`. - * TBD: what to do in case of a name conflict? abort (then to override, `super().__init__` call will be in the beginning) or ignore (then to override, `super().__init__` call will be in the end) +* `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.members.create()`. (If there is a name conflict, it raises an error.) * `Component.signature` collects PEP 526 variable annotations in the class, if any, and returns a signature object constructed from these, or raises an error otherwise. The signature object is created per-instance, not per-class, so that it can be safely mutated if this is a part of the workflow. From c119b00e70f8eef09e8b9d232291c06741e37f41 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 20:13:24 +0000 Subject: [PATCH 08/25] Wording --- text/0002-interfaces.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 87ad4d0..60d7ede 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -385,7 +385,7 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an * A `Signature({"name": Member(...)})` object can be constructed from a name to member mapping. * `signature.members` is a mutable mapping that can be used to alter the description of a non-frozen signature. * `signature.members += {...}` adds members from the given mapping to `signature.members` if the names being added are not already used. Raises `NameError` otherwise. - * `signature.freeze()` (or `signature.members.freeze()`) prevents any further modifications of `signature.members`, enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. + * `signature.freeze()` (or `signature.members.freeze()`) prevents any further modifications of `signature` (and in particular `signature.members`), enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the direction is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. @@ -395,7 +395,7 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an - a warning may be emitted if the `.shape` of the member and the `.shape()` of `object.attr` are not equal - if the member is an interface, the attribute value must be compatible with the signature of the member - if the member's `dimensions` are `(p, q, ...)`, the requirements below hold instead for every result of indexing the attribute value with `[i][j]...` where `i in range(p)`, `j in range(q)`, ... - * `signature.members.create()` creates a dictionary of members from it. This is a helper method that is essentially the part of `create()` that subclasses are unlikely to need to override. For every member of the signature, the dictionary contains a value equal to: + * `signature.members.create()` creates a dictionary of members from it. This is a helper method for the common part of `signature.create()`. For every member of the signature, the dictionary contains a value equal to: * If the member is a port, `Signal(member.shape, reset=member.reset)`. * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `object()` and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. @@ -463,7 +463,7 @@ class Outer(Component): def elaborate(self, platform): m = Module() - component.connect(m, component.forward(self.bus), self.inner.bus) + connect(m, forward(self.bus), self.inner.bus) return m ``` From 04ffa6dd2689f3915d7520de32f765f3b860b482 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 20:13:54 +0000 Subject: [PATCH 09/25] `member.signature` flips if `member.flow == In` --- text/0002-interfaces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 60d7ede..8e8c575 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -373,7 +373,7 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an * the `.is_signature` property be `True`; * the `.shape` property raise `TypeError`; * the `.reset` property raise `TypeError`; - * the `.signature` property return `signature`; + * the `.signature` property return `signature` if `flow` is `Out`, `signature.flip()` if `flow` is `In`. * the `.dimensions` property be `()`. * `member.array(*dimensions)` returns a new `Member` object whose `.dimensions` property is `dimensions`, which is any amount of non-negative numbers, and all other properties are the same as those of `member`. * `member.flip()` returns a new `Member` object whose `.flow` property is `~member.flow`, and all other properties are the same as those of `member`. From 28347758fd020c1da6761a8f6569c591b1593e3a Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 20:44:24 +0000 Subject: [PATCH 10/25] `signature.freeze()` returns self --- text/0002-interfaces.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 8e8c575..c58c93e 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -386,6 +386,13 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an * `signature.members` is a mutable mapping that can be used to alter the description of a non-frozen signature. * `signature.members += {...}` adds members from the given mapping to `signature.members` if the names being added are not already used. Raises `NameError` otherwise. * `signature.freeze()` (or `signature.members.freeze()`) prevents any further modifications of `signature` (and in particular `signature.members`), enabling the caller to rely on a particular layout. It is applied recursively to constituent interfaces. + * It returns `self` to aid assignments in class definition like: + ```python + class X: + signature = Signature({ + ... + }).freeze() + ``` * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the direction is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. From 98ae16eb05abd870c99077bdbc0a1aedbe1fd365 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 21:18:03 +0000 Subject: [PATCH 11/25] Unclear if `Signature.__iter__` and `.__getitem__` are needed. --- text/0002-interfaces.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index c58c93e..8063590 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -495,6 +495,11 @@ To this end, a class `amaranth.lib.component.Component` is introduced: * It is not a function on `amaranth.hdl.dsl.Module` to avoid privileging the standard interface library over any other library that may be written downstream. At the moment nothing in `amaranth.lib` is special in any way other than its name, and preserving this is valuable to the author. +## Unresolved questions + +- Should `Signature.__iter__` and `Signature.__getitem__` exist, or should they be combined and moved to `SignatureMembers[Flipped].flatten`? + + ## Naming questions - Should `Signature` be called `Interface`? From 58678002300410a818fe916d616770ea279caca8 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 27 Jun 2023 21:18:19 +0000 Subject: [PATCH 12/25] Add `Signature.flipped` and self rebinding --- text/0002-interfaces.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 8063590..220f03b 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -395,7 +395,8 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an ``` * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. - * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the direction is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. + * `signature.flipped()` returns `False`. If called as `signature.flip().flipped()`, returns `True`. + * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the flow is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. When calling method `signature.f` as `signature.flip().f`, `self` is the flipped signature. * `signature.compatible(object)` checks whether an arbitrary Python object is compatible with this signature. To be compatible with a signature: - for every member of the signature, the object must have a corresponding attribute - if the member is a port, the attribute value must be a value-castable such that `Value.cast(object.attr)` method returns a `Signal` or a `Const` that has the same width and signedness, and for signals, is not reset-less and has the same reset value as the member From 8e9e232e630ca11ba42f2f82df41145bde9701ef Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 28 Jun 2023 00:52:19 +0000 Subject: [PATCH 13/25] Add bigger name question --- text/0002-interfaces.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 220f03b..a80eec5 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -508,6 +508,8 @@ To this end, a class `amaranth.lib.component.Component` is introduced: - Should `Signature.compatible` be named something else, like `Signature.is_implemented`? - Should `component.forward` be named something else, like `component.forwarded` or `component.forwarding` or `component.evert` or `component.flip`? +There is also a much bigger naming question here. Ideally, `amaranth.lib.component` would be called `amaranth.lib.module` and `amaranth.hdl.dsl.Module` would be called something else. In fact `amaranth.hdl.dsl.Module` is a remarkably poor name for what it does! If it was called amaranth.hdl.dsl.Builder` it would accurately reflect the function *and* not clash with this library. One option is to rename it to `Builder`, leave `Module` as an alias (but not recommend it in educational materials), and then name this library `amaranth.lib.module`. + ## Future work From ae63fe639588006aeedbca82da7c0d6ca547a791 Mon Sep 17 00:00:00 2001 From: Catherine Date: Sat, 12 Aug 2023 18:41:39 +0000 Subject: [PATCH 14/25] `lib.component`->`lib.wiring`, update `connect()` spec, update naming questions --- text/0002-interfaces.md | 43 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index a80eec5..0098e38 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -55,7 +55,7 @@ Since these drawbacks are entrenched in the public API and make `Record` nearly Although some HDLs and IRs (Migen, Chisel, FIRRTL, ...) choose to use the same basic aggregate data type to represent _structured data_ and _directional interfaces_, these mechanisms are in direct conflict. Complex forms of structured data, such as unions, are incompatible with associating directionality independently with every leaf member; and the non-directional nature of stored data requires complicated and error-prone rules when it can become a part of a directional connection. -Amaranth, instead, opts to include two superficially similar mechanisms for defining and accessing hierarchical aggregate data: `amaranth.lib.data` (RFC #1) and `amaranth.lib.component` (this RFC). `amaranth.lib.data` provides _data views_ that reinterpret bit containers as complex aggregates, and entirely avoids directionality. `amaranth.lib.component` provides _signatures_ that give a concrete shape to signals at component boundaries, and always treats them as directional. +Amaranth, instead, opts to include two superficially similar mechanisms for defining and accessing hierarchical aggregate data: `amaranth.lib.data` (RFC #1) and `amaranth.lib.wiring` (this RFC). `amaranth.lib.data` provides _data views_ that reinterpret bit containers as complex aggregates, and entirely avoids directionality. `amaranth.lib.wiring` provides _signatures_ that give a concrete shape to signals at component boundaries, and always treats them as directional. When connections are made strictly between an output and a correspondingly named input, interfaces gain a dualistic nature: every connection is made between two interfaces whose port directions are the inverse of each other, and which are identical otherwise. To describe interfaces without repeating oneself, then, one has to pick an arbitrarily preferred directionality (and stick with it). Many interfaces are asymmetric, with data flowing from a source to a sink, or transactions issued from an initiator to a target. Amaranth picks the _source_ or _initiator_ perspective; an interface, examined in isolation, defines as outputs the signals that would be outputs of an initiator (and inputs of a target). Then, when an interface with true (non-flipped) directionality describes a component's output, the same interface with inverse (flipped) directionality symmetrically describes an input. @@ -103,7 +103,7 @@ This RFC proposes a way of describing signal directions that can be applied to a To describe signal directions, only a single addition is needed: the `signature` property: ```python -from amaranth.lib.component import Signature, In, Out +from amaranth.lib.wiring import Signature, In, Out class SequenceSource(Elaboratable): @@ -156,7 +156,7 @@ This is tedious, verbose, and error-prone. It is possible to define an applicati This RFC introduces a way to describe interfaces (collections of directional signals; more on this later) and a single operation: *connecting*. The code above now transforms into: ```python -from amaranth.lib.component import connect +from amaranth.lib.wiring import connect m = Module() @@ -280,7 +280,7 @@ class AbsoluteProcessor(Elaboratable): However, since the interface of `AbsoluteProcessor` as a whole can itself be described as a signature, it is possible to further shorten it by deriving from `component.Component` instead of `Elaboratable`, in which case the attributes will be filled in from the signature automatically: ```python -from amaranth.lib.component import Component +from amaranth.lib.wiring import Component class AbsoluteProcessor(Component): @@ -321,7 +321,7 @@ class AbsoluteProcessor(Component): All of the import statements in the code examples above can be replaced with: ```python -from amaranth.lib.component import * +from amaranth.lib.wiring import * ``` @@ -349,7 +349,7 @@ A single elaboratable object will often have several interfaces; e.g. a peripher ### Interface description -Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, and two classes, `amaranth.lib.component.Member` and `amaranth.lib.component.Signature`: +Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and two classes, `amaranth.lib.wiring.Member` and `amaranth.lib.wiring.Signature`: * `Flow` is an enumeration with two values, `In` and `Out`. @@ -406,21 +406,21 @@ Interfaces are described using an enumeration, `amaranth.lib.component.Flow`, an * `signature.members.create()` creates a dictionary of members from it. This is a helper method for the common part of `signature.create()`. For every member of the signature, the dictionary contains a value equal to: * If the member is a port, `Signal(member.shape, reset=member.reset)`. * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. - * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `object()` and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. + * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `amaranth.lib.wiring.Interface()` (which is essentially an empty class) and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. ### Interface connection -Interface objects may be connected to each other using the `amaranth.lib.component.connect(m, first, *rest)` free function. +Interface objects may be connected to each other using the `amaranth.lib.wiring.connect(m, *objects)` free function. This function connects interface objects that satisfy the following requirements: -* For every port member with a given `path` and `first_shape` in `first`, there is a port member with the same `path` in each of `rest` with shape `rest_shape`, where `Shape.cast(first_shape) == Shape.cast(rest_shape)`, and there are no other members in any of `rest`. In other words, the width and signedness of all of port members must be equal, but the shape-castable objects specifying the width and signedness do not have to be. - * It is expected that any mismatch in signatures will be resolved (through wrappers, mutating signature members, or otherwise) before interfaces are being connected. -* Either: - 1. There is exactly one interface object in `rest`, and for every pair of members `first_member` and `rest_member`, `first_member.flow == ~rest_member.flow`. - 2. There is any amount of interfaces in `rest`, and for every pair of members `first_member` and `rest_member`, `first_member.flow == Out` and `rest_member.flow == In`. +* The set of members (considered by their paths) is exactly the same for each of the objects. +* For each given path, all members are either signature members or port members. +* For each given path where all members are port members, the width of every member with the same path is equal, though the exact types of the objects returned by the `.shape` property may differ. +* For each given path where all members are port members, the reset values of all members with the same path must match. +* For each given path where all members are port members, exactly one member has an `Out` flow. -In both of the cases (1) and (2), the function, for each pair of an input port and an output of port with the same path, the function connects these as follows: +The `Out` port member is connected to the `In` port members with the same path as follows: ```python m.d.comb += output_port.eq(input_port) ``` @@ -456,9 +456,9 @@ class Inner(Component): ... ``` -In this case, `amaranth.lib.component.connect(...)` won't help, since an output needs to be connected to an output, and an input to an input. +In this case, `amaranth.lib.wiring.connect(...)` won't help, since an output needs to be connected to an output, and an input to an input. -An additional function `amaranth.lib.component.forward(obj)` is added to assist in this case. It returns a proxy object `obj_forward` where `obj_forward.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: +An additional function `amaranth.lib.wiring.forward(obj)` is added to assist in this case. It returns a proxy object `obj_forward` where `obj_forward.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: ```python class Outer(Component): @@ -483,7 +483,7 @@ This RFC in effect introduces a particular kind of elaboratable object: one that 2. It simplifies and standardizes assignment of the interface attributes, making the `signature` property the single source of truth for the module's interface; 3. It makes it easy to convert a single standalone elaboratable to Verilog. -To this end, a class `amaranth.lib.component.Component` is introduced: +To this end, a class `amaranth.lib.wiring.Component` is introduced: * `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.members.create()`. (If there is a name conflict, it raises an error.) * `Component.signature` collects PEP 526 variable annotations in the class, if any, and returns a signature object constructed from these, or raises an error otherwise. The signature object is created per-instance, not per-class, so that it can be safely mutated if this is a part of the workflow. @@ -492,7 +492,7 @@ To this end, a class `amaranth.lib.component.Component` is introduced: - Do nothing. `Record` will continue to be used alongside the continued proliferation of ad-hoc implementations of similar functionality, and continue to impair the use of Amaranth components together. -- Replace the `interface.lib.component.connect` free function with a function `amaranth.hdl.dsl.Module.connect`. +- Replace the `amaranth.lib.wiring.connect` free function with a function `amaranth.hdl.dsl.Module.connect`. * It is not a function on `amaranth.hdl.dsl.Module` to avoid privileging the standard interface library over any other library that may be written downstream. At the moment nothing in `amaranth.lib` is special in any way other than its name, and preserving this is valuable to the author. @@ -503,12 +503,9 @@ To this end, a class `amaranth.lib.component.Component` is introduced: ## Naming questions -- Should `Signature` be called `Interface`? - - The object returned by `Signature.create()` is an interface, not the signature itself +- Should `amaranth.lib.wiring` be called something else, like `amaranth.lib.bus` or `amaranth.lib.component`? - Should `Signature.compatible` be named something else, like `Signature.is_implemented`? -- Should `component.forward` be named something else, like `component.forwarded` or `component.forwarding` or `component.evert` or `component.flip`? - -There is also a much bigger naming question here. Ideally, `amaranth.lib.component` would be called `amaranth.lib.module` and `amaranth.hdl.dsl.Module` would be called something else. In fact `amaranth.hdl.dsl.Module` is a remarkably poor name for what it does! If it was called amaranth.hdl.dsl.Builder` it would accurately reflect the function *and* not clash with this library. One option is to rename it to `Builder`, leave `Module` as an alias (but not recommend it in educational materials), and then name this library `amaranth.lib.module`. +- Should `amaranth.lib.wiring.forward` be named something else, like `amaranth.lib.wiring.forwarded` or `amaranth.lib.wiring.forwarding` or `amaranth.lib.wiring.flip`? ## Future work From 4cfa45ecd618f1569b3bc6da5e92e4bb2770c599 Mon Sep 17 00:00:00 2001 From: Catherine Date: Sat, 12 Aug 2023 18:44:19 +0000 Subject: [PATCH 15/25] clarify --- text/0002-interfaces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 0098e38..59fdf17 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -406,7 +406,7 @@ Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and t * `signature.members.create()` creates a dictionary of members from it. This is a helper method for the common part of `signature.create()`. For every member of the signature, the dictionary contains a value equal to: * If the member is a port, `Signal(member.shape, reset=member.reset)`. * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. - * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `amaranth.lib.wiring.Interface()` (which is essentially an empty class) and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. + * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `amaranth.lib.wiring.Interface()` (which is an empty class) and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. ### Interface connection From 9bffd2f38ac2ff57a0adaa864ae1040874d9ae50 Mon Sep 17 00:00:00 2001 From: Catherine Date: Mon, 14 Aug 2023 22:44:22 +0000 Subject: [PATCH 16/25] update after meeting --- text/0002-interfaces.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 59fdf17..27491a2 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -239,7 +239,7 @@ class AbsoluteProcessor(Elaboratable): }) ``` -To be compatible with this signature, an `AbsoluteProcessor` instance must have an `i` attribute compatible with a `StreamSignature(signed(16)).flip()`, and an `o` attribute compatible with a `StreamSignature(unsigned(16))`. These could be defined manually: +To be compliant with this signature, an `AbsoluteProcessor` instance must have an `i` attribute compliant with a `StreamSignature(signed(16)).flip()`, and an `o` attribute compliant with a `StreamSignature(unsigned(16))`. These could be defined manually: ```python class AbsoluteProcessor(Elaboratable): @@ -338,11 +338,11 @@ It also introduces a number of technical terms: * An _interface_ (a concept) is a shared boundary across which several Amaranth components exchange data. It is comprised of a set of signals and the invairants that govern their use. * An _interface object_ (an implementation of the concept) a Python object that includes: 1. attributes whose value is an Amaranth value-castable, or another interface; - 2. a `signature` attribute whose value is a _signature_ that is _compatible_ with this object; + 2. a `signature` attribute whose value is a _signature_ that is _compliant_ with this object; 3. a description of the invariants applying to its use (in form of documentation, testbenches, formal tests, etc.). * A _signature_ is a `Signature` instance describing requirements applicable to a hierarchy of interace objects. * A _signature member_ is a `Member` instance describing requirements applicable to a single attribute of an interface object. Two kinds of signature members exist: port members (requiring the value of the attribute to be a `Signal`), and interface members (requiring the value of the attribute to be another interface object). -* A signature is _compatible_ with an object (therefore making it an interface object) if every member of the signature object corresponds to an attribute of the object whose value fits the requirements. +* An object is _compliant_ with a signature (therefore making it an interface object) if every member of the signature corresponds to an attribute of the object whose value fits the requirements. A single elaboratable object will often have several interfaces; e.g. a peripheral can have a CSR and/or Wishbone bus interface, and a pin interface. However, the elaboratable object itself can be an interface object as well, which makes it easy to convert it to Verilog and use standalone since its signature defines the ports the Verilog module needs to have. @@ -393,15 +393,13 @@ Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and t ... }).freeze() ``` - * `signature.__iter__()` yields `path` recursively for every member and sub-member. A member's path is a tuple containing every name in the chain of attribute accesses required to reach the member. Members are yielded in an ascending lexicographical order. An interface member's path is yielded before the paths of its sub-members are. - * `signature.__getitem__(*path)` looks up a member by its path. The flow of the member is flipped as many times as there are `In` signatures between the topmost signature and the signature of the member. * `signature.flipped()` returns `False`. If called as `signature.flip().flipped()`, returns `True`. * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the flow is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. When calling method `signature.f` as `signature.flip().f`, `self` is the flipped signature. - * `signature.compatible(object)` checks whether an arbitrary Python object is compatible with this signature. To be compatible with a signature: + * `signature.is_compliant(object)` checks whether an arbitrary Python object is compliant with this signature. To be compliant with a signature: - for every member of the signature, the object must have a corresponding attribute - if the member is a port, the attribute value must be a value-castable such that `Value.cast(object.attr)` method returns a `Signal` or a `Const` that has the same width and signedness, and for signals, is not reset-less and has the same reset value as the member - a warning may be emitted if the `.shape` of the member and the `.shape()` of `object.attr` are not equal - - if the member is an interface, the attribute value must be compatible with the signature of the member + - if the member is an interface, the attribute value must be compliant with the signature of the member - if the member's `dimensions` are `(p, q, ...)`, the requirements below hold instead for every result of indexing the attribute value with `[i][j]...` where `i in range(p)`, `j in range(q)`, ... * `signature.members.create()` creates a dictionary of members from it. This is a helper method for the common part of `signature.create()`. For every member of the signature, the dictionary contains a value equal to: * If the member is a port, `Signal(member.shape, reset=member.reset)`. @@ -498,14 +496,28 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: ## Unresolved questions -- Should `Signature.__iter__` and `Signature.__getitem__` exist, or should they be combined and moved to `SignatureMembers[Flipped].flatten`? +- Should we have `{Signature,FlippedSignature}.flipped`? + - Easy enough to emulate using `isinstance`; not a lot of use envisioned +- Should we move the contents of `Signature.create` to `Interface.__init__`? + - Little harm; easily overridden; makes `Signature.create` really simple; makes `Interface` more useful ## Naming questions - Should `amaranth.lib.wiring` be called something else, like `amaranth.lib.bus` or `amaranth.lib.component`? -- Should `Signature.compatible` be named something else, like `Signature.is_implemented`? -- Should `amaranth.lib.wiring.forward` be named something else, like `amaranth.lib.wiring.forwarded` or `amaranth.lib.wiring.forwarding` or `amaranth.lib.wiring.flip`? + - bus is short, but not every interface is a bus interface; component (or module, really) puts too much emphasis on the things being interfaced, rather than the interfaces (@jfng) + - i wouldn't want the bus keyword to already be taken in my namespaces (@jfng) + - I guess my point is mostly that bus is not the opposite of data, but wiring is (@whitequark) + - I don't like how long "component" is (@whitequark) +- Should `Signature.compatible` be named something else, like `Signature.is_implemented_by`, `Signature.is_compliant`, `Signature.complies_with`? + - `Signature.compatible` misses an `is_` and does not look like a query method (@jfng) + - I mean, "compatible" could mean that two signatures could be connected together. when checking if an object is compliant to a signature, directions also matters (@zyp) + - `Signature.complies_with` reverses subject and object (@zyp) + - `Signature.is_implemented_by` is verbose (@jfng) +- Should `amaranth.lib.wiring.forward` be named something else, like `amaranth.lib.wiring.forwarded` or `amaranth.lib.wiring.forwarding` or `amaranth.lib.wiring.flip` or `amaranth.lib.wiring.transpose` or ``amaranth.lib.wiring.transpose` or `amaranth.lib.wiring.inner`? + - having two essentially unrelated operations called `flip` when one is already confusing is too much (@whitequark) + - reflective programming is a thing (@zyp) + - inner(inner(interface)) to flip it back to the original wouldn't make much sense (@zyp) ## Future work From 8407b963d6a1f2b8fd938b92e873e69fecd30567 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 15 Aug 2023 16:15:31 +0000 Subject: [PATCH 17/25] add a note on connecting constant port members --- text/0002-interfaces.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 27491a2..025a46f 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -418,11 +418,13 @@ This function connects interface objects that satisfy the following requirements * For each given path where all members are port members, the reset values of all members with the same path must match. * For each given path where all members are port members, exactly one member has an `Out` flow. -The `Out` port member is connected to the `In` port members with the same path as follows: +If the `In` port member is a signal, it is connected to the `Out` port member with the same path as follows: ```python -m.d.comb += output_port.eq(input_port) +m.d.comb += input_port.eq(output_port) ``` +If the `In` port member is a constant, no connection is actually made. The `Out` port member with the same path (if any) must be a constant with the same value. + ### Interface forwarding From 536f4f8f9c76ac9da02d9a953da5cc0b7524043b Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 17 Aug 2023 14:50:27 +0000 Subject: [PATCH 18/25] update guide level section for transposing --- text/0002-interfaces.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 025a46f..eb0c49c 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -426,9 +426,9 @@ m.d.comb += input_port.eq(output_port) If the `In` port member is a constant, no connection is actually made. The `Out` port member with the same path (if any) must be a constant with the same value. -### Interface forwarding +### Interface transposing -In some cases, an outer elaboratable object creates an inner elaboratable object and _forwards_ an interface of the inner object like this: +In some cases, an outer elaboratable object creates an inner elaboratable object and _transposes_ an interface of the inner object like this: ```python class Outer(Component): @@ -458,7 +458,7 @@ class Inner(Component): In this case, `amaranth.lib.wiring.connect(...)` won't help, since an output needs to be connected to an output, and an input to an input. -An additional function `amaranth.lib.wiring.forward(obj)` is added to assist in this case. It returns a proxy object `obj_forward` where `obj_forward.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: +An additional function `amaranth.lib.wiring.transpose(obj)` is added to assist in this case. It returns a proxy object `obj_transposed` where `obj_transposed.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: ```python class Outer(Component): @@ -471,7 +471,7 @@ class Outer(Component): def elaborate(self, platform): m = Module() - connect(m, forward(self.bus), self.inner.bus) + connect(m, transpose(self.bus), self.inner.bus) return m ``` From b2e9f41f64c56fb72be219fcf0eea9aec71df652 Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 17 Aug 2023 15:01:22 +0000 Subject: [PATCH 19/25] add unresolved question about component and MRO --- text/0002-interfaces.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index eb0c49c..122cd82 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -500,6 +500,7 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: - Should we have `{Signature,FlippedSignature}.flipped`? - Easy enough to emulate using `isinstance`; not a lot of use envisioned +- Should `Component` collect annotations from the MRO and not just the class itself? - Should we move the contents of `Signature.create` to `Interface.__init__`? - Little harm; easily overridden; makes `Signature.create` really simple; makes `Interface` more useful From 0ac293ffdfdbd73ad18aa6a610e21722e5f8b0ac Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 17 Aug 2023 16:44:51 +0000 Subject: [PATCH 20/25] add `Interface` and move `signature.create()` logic to `Interface.__init__` --- text/0002-interfaces.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 122cd82..e97f516 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -404,7 +404,9 @@ Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and t * `signature.members.create()` creates a dictionary of members from it. This is a helper method for the common part of `signature.create()`. For every member of the signature, the dictionary contains a value equal to: * If the member is a port, `Signal(member.shape, reset=member.reset)`. * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. - * `signature.create()` creates an interface object from this signature. To do this, it creates a fresh `amaranth.lib.wiring.Interface()` (which is an empty class) and replaces its dictionary with the result of `signature.members.create()`. This method is expected to be routinely overridden in `Signature` subclasses to perform actions specific to a particular signature. + * `signature.create()` creates an interface object from this signature. To do this, it calls the constructor of `Interface` described below. This method is expected to be routinely overridden in `Signature` subclasses to instantiate subclasses of `Interface`. + +Any object can be an interface object if it has the appropriate `signature` property. However, an `amaranth.lib.wiring.Interface` class is introduced, serving two purposes: instantiating interfaces from an anonymous signature, and serving as a convenient base class for custom interface classes. The `Interface` class implements only the `__init__()` method, accepting a signature as a parameter. It assigns `self.signature` to be that signature, and for each item in `signature.members.create()` it creates a corresponding attribute on `self`. ### Interface connection @@ -501,8 +503,6 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: - Should we have `{Signature,FlippedSignature}.flipped`? - Easy enough to emulate using `isinstance`; not a lot of use envisioned - Should `Component` collect annotations from the MRO and not just the class itself? -- Should we move the contents of `Signature.create` to `Interface.__init__`? - - Little harm; easily overridden; makes `Signature.create` really simple; makes `Interface` more useful ## Naming questions From 7b46aef2d3c8af1bd41281b261b2f87d04e676b6 Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 17 Aug 2023 16:46:11 +0000 Subject: [PATCH 21/25] extend `Component.signature` to take annotations from the MRO --- text/0002-interfaces.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index e97f516..7c8029f 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -487,7 +487,7 @@ This RFC in effect introduces a particular kind of elaboratable object: one that To this end, a class `amaranth.lib.wiring.Component` is introduced: * `Component.__init__` (typically called as `super().__init__()`) updates `self.__dict__` with the result of `self.signature.members.create()`. (If there is a name conflict, it raises an error.) -* `Component.signature` collects PEP 526 variable annotations in the class, if any, and returns a signature object constructed from these, or raises an error otherwise. The signature object is created per-instance, not per-class, so that it can be safely mutated if this is a part of the workflow. +* `Component.signature` collects PEP 526 variable annotations in the class's method resolution order chain up to `Component`, if any, and returns a signature object constructed from these, or raises an error otherwise. The signature object is created per-instance, not per-class, so that it can be safely mutated if this is a part of the workflow. ## Alternatives and rationale @@ -502,7 +502,6 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: - Should we have `{Signature,FlippedSignature}.flipped`? - Easy enough to emulate using `isinstance`; not a lot of use envisioned -- Should `Component` collect annotations from the MRO and not just the class itself? ## Naming questions From 1f6c8277dfab0141461644e1d943f5ef17d4ac64 Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 17 Aug 2023 17:32:04 +0000 Subject: [PATCH 22/25] remove `sig.flipped`, recommend `type(self) is FlippedSignature` --- text/0002-interfaces.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 7c8029f..b06276a 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -393,7 +393,6 @@ Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and t ... }).freeze() ``` - * `signature.flipped()` returns `False`. If called as `signature.flip().flipped()`, returns `True`. * `signature.flip()` returns a signature where every member is `member.flip()`ped. The exact object returned is a proxy object that overrides the methods and attributes defined here such that the flow is flipped, and otherwise forwards attribute accesses untouched. That is, `signature.x = ` and `signature.flip().x = ` both define an attribute on the original `signature` object, and never on the proxy object alone. When calling method `signature.f` as `signature.flip().f`, `self` is the flipped signature. * `signature.is_compliant(object)` checks whether an arbitrary Python object is compliant with this signature. To be compliant with a signature: - for every member of the signature, the object must have a corresponding attribute @@ -406,6 +405,8 @@ Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and t * If the member is a signature, `member.signature.create()` for `Out` members, and `member.signature.flip().create()` for `In` members. * `signature.create()` creates an interface object from this signature. To do this, it calls the constructor of `Interface` described below. This method is expected to be routinely overridden in `Signature` subclasses to instantiate subclasses of `Interface`. +All of the methods that can be called on `signature` can be called on the object returned by `signature.flip()`, and `self` in that case is `signature.flip()`. This means that in a method defined on a subclass of `Signature`, `self` can be an instance of that type, or an instance of a different type, `FlippedSignature`, which implements the flipping behavior. In the rare case where it is useful to determine which one it is, it is possible to use `type(self) is amaranth.lib.wiring.FlippedSignature`. + Any object can be an interface object if it has the appropriate `signature` property. However, an `amaranth.lib.wiring.Interface` class is introduced, serving two purposes: instantiating interfaces from an anonymous signature, and serving as a convenient base class for custom interface classes. The `Interface` class implements only the `__init__()` method, accepting a signature as a parameter. It assigns `self.signature` to be that signature, and for each item in `signature.members.create()` it creates a corresponding attribute on `self`. @@ -500,8 +501,7 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: ## Unresolved questions -- Should we have `{Signature,FlippedSignature}.flipped`? - - Easy enough to emulate using `isinstance`; not a lot of use envisioned +None. ## Naming questions From c53abf78f7ca67f990804ebd80c7102e214ce945 Mon Sep 17 00:00:00 2001 From: Catherine Date: Sat, 19 Aug 2023 00:03:21 +0000 Subject: [PATCH 23/25] add (back) unresolved question about `transpose` --- text/0002-interfaces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index b06276a..c79997f 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -501,7 +501,7 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: ## Unresolved questions -None. +- The `amaranth.lib.wiring.transpose` is a really badly named function. We need a better one. ## Naming questions From c52952f256287889add65a9261eaefff88ae48dc Mon Sep 17 00:00:00 2001 From: Catherine Date: Sat, 19 Aug 2023 02:04:59 +0000 Subject: [PATCH 24/25] describe `.array` as composable --- text/0002-interfaces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index c79997f..4c001cc 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -375,7 +375,7 @@ Interfaces are described using an enumeration, `amaranth.lib.wiring.Flow`, and t * the `.reset` property raise `TypeError`; * the `.signature` property return `signature` if `flow` is `Out`, `signature.flip()` if `flow` is `In`. * the `.dimensions` property be `()`. - * `member.array(*dimensions)` returns a new `Member` object whose `.dimensions` property is `dimensions`, which is any amount of non-negative numbers, and all other properties are the same as those of `member`. + * `member.array(*dimensions)` returns a new `Member` object whose `.dimensions` property is `dimensions`, which is any amount of non-negative numbers, and all other properties are the same as those of `member`. Calling `.array()` on a member with dimensions prepends the new dimensions before the old ones, for composability. * `member.flip()` returns a new `Member` object whose `.flow` property is `~member.flow`, and all other properties are the same as those of `member`. * A `Signature(...)` object describes an interface comprised of named members: ports and nested interfaces (which themselves are described using signature objects). From 379d41946241a974ba7bd6733c82bc3a10ff6337 Mon Sep 17 00:00:00 2001 From: Catherine Date: Sat, 19 Aug 2023 02:06:29 +0000 Subject: [PATCH 25/25] go back to `flipped()` from `transpose()` --- text/0002-interfaces.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/text/0002-interfaces.md b/text/0002-interfaces.md index 4c001cc..9f68ce8 100644 --- a/text/0002-interfaces.md +++ b/text/0002-interfaces.md @@ -429,9 +429,9 @@ m.d.comb += input_port.eq(output_port) If the `In` port member is a constant, no connection is actually made. The `Out` port member with the same path (if any) must be a constant with the same value. -### Interface transposing +### Forwarding interfaces -In some cases, an outer elaboratable object creates an inner elaboratable object and _transposes_ an interface of the inner object like this: +In some cases, an outer elaboratable object creates an inner elaboratable object and exposes an interface of the inner object as its own: ```python class Outer(Component): @@ -461,7 +461,7 @@ class Inner(Component): In this case, `amaranth.lib.wiring.connect(...)` won't help, since an output needs to be connected to an output, and an input to an input. -An additional function `amaranth.lib.wiring.transpose(obj)` is added to assist in this case. It returns a proxy object `obj_transposed` where `obj_transposed.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: +An additional function `amaranth.lib.wiring.flipped(obj)` is added to assist in this case. It returns a proxy object `obj_flipped` where `obj_flipped.signature` equals `obj.signature.flip()`, and everything else is forwarded identically otherwise. So, the `Outer.elaborate` method can be rewritten as: ```python class Outer(Component): @@ -474,7 +474,7 @@ class Outer(Component): def elaborate(self, platform): m = Module() - connect(m, transpose(self.bus), self.inner.bus) + connect(m, flipped(self.bus), self.inner.bus) return m ``` @@ -499,11 +499,6 @@ To this end, a class `amaranth.lib.wiring.Component` is introduced: * It is not a function on `amaranth.hdl.dsl.Module` to avoid privileging the standard interface library over any other library that may be written downstream. At the moment nothing in `amaranth.lib` is special in any way other than its name, and preserving this is valuable to the author. -## Unresolved questions - -- The `amaranth.lib.wiring.transpose` is a really badly named function. We need a better one. - - ## Naming questions - Should `amaranth.lib.wiring` be called something else, like `amaranth.lib.bus` or `amaranth.lib.component`?