From 397c7b4d11e40b91f2c60d761283f11f2935b832 Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 18 Aug 2023 00:12:18 +0000 Subject: [PATCH] lib.wiring: document amaranth-lang/rfcs#2. WIP --- amaranth/lib/wiring.py | 264 ++++++++++++++++++++++++++++++++++++++++- docs/conf.py | 5 + docs/stdlib.rst | 1 + docs/stdlib/wiring.rst | 102 ++++++++++++++++ 4 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 docs/stdlib/wiring.rst diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py index c8a42870f3..7fefd5b0ca 100644 --- a/amaranth/lib/wiring.py +++ b/amaranth/lib/wiring.py @@ -14,10 +14,47 @@ class Flow(enum.Enum): + """Direction of data flow. This enumeration has two values, :attr:`Out` and :attr:`In`, + the meaning of which depends on the context in which they are used. + """ + + #: `Outgoing` data flow. + #: + #: When included in a standalone :class:`Signature`, a port :class:`Member` with an :attr:`Out` + #: data flow carries data from an `initiator` to a `responder`. That is, the signature + #: describes the initiator `driving` the signal and the responder `sampling` the signal. + #: + #: When used as the flow of a signature :class:`Member`, indicates that the data flow of + #: the port members of the inner signature `remains the same`. + #: + #: When included in the ``signature`` property of an :class:`Elaboratable`, the signature + #: describes the elaboratable `driving` the corresponding signal. That is, the elaboratable is + #: treated as the `initiator`. Out = 0 + + #: `Incoming` data flow. + #: + #: When included in a standalone :class:`Signature`, a port :class:`Member` with an :attr:`In` + #: data flow carries data from an `responder` to a `initiator`. That is, the signature + #: describes the initiator `sampling` the signal and the responder `driving` the signal. + #: + #: When used as the flow of a signature :class:`Member`, indicates that the data flow of + #: the port members of the inner signature `is flipped`. + #: + #: When included in the ``signature`` property of an :class:`Elaboratable`, the signature + #: describes the elaboratable `sampling` the corresponding signal. That is, the elaboratable is + #: treated as the `initiator`, the same as in the :attr:`Out` case. In = 1 def flip(self): + """Flip the direction of data flow. + + Returns + ------- + :class:`Flow` + :attr:`In` if called as :pycode:`Out.flip()`; :attr:`Out` if called as + :pycode:`In.flip()`. + """ if self == Out: return In if self == In: @@ -25,6 +62,13 @@ def flip(self): assert False # :nocov: def __call__(self, description, *, reset=None): + """Create a :class:`Member` with this data flow. + + Returns + ------- + :class:`Member` + :pycode:`Member(self, description, reset=reset)` + """ return Member(self, description, reset=reset) def __repr__(self): @@ -34,12 +78,31 @@ def __str__(self): return self.name -In = Flow.In +#: A shortcut for importing :attr:`Flow.Out` as :data:`amaranth.lib.wiring.Out`. Out = Flow.Out +#: A shortcut for importing :attr:`Flow.In` as :data:`amaranth.lib.wiring.In`. +In = Flow.In + + @final class Member: + """Description of a signature member. + + This class is a discriminated union: its instances describe either a `port member` or + a `signature member`, and accessing properties for the wrong kind of member raises + an :exc:`AttributeError`. + + The class is created from a `description`: a :class:`Signature` instance (in which case + the :class:`Member` is created as a signature member), or + a :ref:`shape-castable ` object (in which case it is created as a port + member). After creation the :class:`Member` instance cannot be modified. + + Although instances can be created directly, most often they will be created through + :data:`In` and :data:`Out`, e.g. :pycode:`In(unsigned(1))` or + :pycode:`Out(stream.Signature(RGBPixel))`. + """ def __init__(self, flow, description, *, reset=None, _dimensions=()): self._flow = flow self._description = description @@ -74,10 +137,38 @@ def __init__(self, flow, description, *, reset=None, _dimensions=()): raise ValueError(f"A signature member cannot have a reset value") def flip(self): + """Flip the data flow of this member. + + Returns + ------- + :class:`Member` + A new :pycode:`member` with :pycode:`member.flow` equal to :pycode:`self.flow.flip()`, + and identical to :pycode:`self` other than that. + """ return Member(self._flow.flip(), self._description, reset=self._reset, _dimensions=self._dimensions) def array(self, *dimensions): + """Add array dimensions to this member. + + The dimensions passed to this method are `prepended` to the existing dimensions. + For example, :pycode:`Out(1).array(2)` describes an array of 2 elements, whereas both + :pycode:`Out(1).array(2, 3)` and :pycode:`Out(1).array(3).array(2)` both describe + a two-dimensional array of 2 by 3 elements. + + Dimensions are passed to :meth:`array` in the order in which they would be indexed. + That is, :pycode:`.array(x, y)` creates a member that can be indexed up to + :pycode:`[x-1][y-1]`. + + The :meth:`array` method is composable: calling :pycode:`member.array(x)` describes + an array of :pycode:`x` members even if :pycode:`member` was already an array. + + Returns + ------- + :class:`Member` + A new :pycode:`member` with :pycode:`member.dimensions` extended by + :pycode:`dimensions`, and identical to :pycode:`self` other than that. + """ for dimension in dimensions: if not (isinstance(dimension, int) and dimension >= 0): raise TypeError(f"Member array dimensions must be non-negative integers, " @@ -87,30 +178,88 @@ def array(self, *dimensions): @property def flow(self): + """Data flow of this member. + + Returns + ------- + :class:`Flow` + """ return self._flow @property def is_port(self): + """Whether this is a description of a port member. + + Returns + ------- + :class:`bool` + :pycode:`True` if this is a description of a port member, + :pycode:`False` if this is a description of a signature member. + """ return not isinstance(self._description, Signature) @property def is_signature(self): + """Whether this is a description of a signature member. + + Returns + ------- + :class:`bool` + :pycode:`True` if this is a description of a signature member, + :pycode:`False` if this is a description of a port member. + """ return isinstance(self._description, Signature) @property def shape(self): + """Shape of a port member. + + Returns + ------- + :ref:`shape-castable object ` + The shape that was provided when constructing this :class:`Member`. + + Raises + ------ + :exc:`AttributeError` + If :pycode:`self` describes a signature member. + """ if self.is_signature: raise AttributeError(f"A signature member does not have a shape") return self._description @property def reset(self): + """Reset value of a port member. + + Returns + ------- + :ref:`const-castable object ` + The reset value that was provided when constructing this :class:`Member`. + + Raises + ------ + :exc:`AttributeError` + If :pycode:`self` describes a signature member. + """ if self.is_signature: raise AttributeError(f"A signature member does not have a reset value") return self._reset @property def signature(self): + """Signature of a signature member. + + Returns + ------- + :class:`Signature` + The signature that was provided when constructing this :class:`Member`. + + Raises + ------ + :exc:`AttributeError` + If :pycode:`self` describes a port member. + """ if self.is_port: raise AttributeError(f"A port member does not have a signature") if self.flow == Out: @@ -121,6 +270,16 @@ def signature(self): @property def dimensions(self): + """Array dimensions. + + A member will usually have no dimensions; in this case it does not describe an array. + A single dimension describes one-dimensional array, and so on. + + Returns + ------- + :class:`tuple` of :class:`int` + Dimensions, if any, of this member, from most to least major. + """ return self._dimensions def __eq__(self, other): @@ -141,13 +300,21 @@ def __repr__(self): @final class SignatureError(Exception): - pass + """ + This exception is raised when an invalid operation specific to signature manipulation is + performed with :class:`SignatureMembers`, such as adding a member to a frozen signature. + Other exceptions, such as :exc:`TypeError` or :exc:`NameError`, will still be raised where + appropriate. + """ # Inherits from Mapping and not MutableMapping because it's only mutable in a very limited way # and most of the methods (except for `update`) added by MutableMapping are useless. @final class SignatureMembers(Mapping): + """ + .. todo:: Write this + """ def __init__(self, members=()): self._dict = dict() self._frozen = False @@ -245,10 +412,46 @@ def __repr__(self): @final class FlippedSignatureMembers(Mapping): + """A mapping of signature member names to their descriptions, with the directions flipped. + + Although an instance of :class:`FlippedSignatureMembers` could be created directly, it will + be usually created by a call to :meth:`SignatureMembers.flip`. + + This container is a wrapper around :class:`SignatureMembers` that contains the same members + as the inner mapping, but flips their data flow when they are accessed. It is useful because + :class:`SignatureMembers` is a mutable mapping, and if it was copied (rather than wrapped) + by :meth:`SignatureMembers.flip`, adding members would cause the flipped copy to become + outdated. + + For example: + + .. testcode:: + + members = wiring.SignatureMembers({"foo": Out(1)}) + + flipped_members = members.flip() + assert flipped_members["foo"].flow == In + + flipped_members["bar"] = In(2) + assert members["bar"] == Out(2) + + This class implements the same methods, with the same functionality (other than the flipping of + the data flow), as the :class:`SignatureMembers` class; see the documentation for that class + for details. + """ + def __init__(self, unflipped): self.__unflipped = unflipped def flip(self): + """ + Flips this mapping back to the original one. + + Returns + ------- + :class:`SignatureMembers` + :pycode:`unflipped` + """ return self.__unflipped # See the note below. @@ -327,7 +530,29 @@ def _format_shape(shape): class SignatureMeta(type): + """A metaclass for :class:`Signature` that makes :class:`FlippedSignature` its + 'virtual subclass'. + + The object returned by :meth:`Signature.flip` is an instance of :class:`FlippedSignature`. + It implements all of the methods :class:`Signature` has, and for subclasses of + :class:`Signature`, it implements all of the methods defined on the subclass as well. + This makes it effectively a subtype of :class:`Signature` (or a derived class of it), but this + relationship is not captured by the Python type system: :class:`FlippedSignature` only has + :class:`object` as its base class. + + This metaclass extends :func:`issubclass` and :func:`isinstance` so that they take into + account the subtyping relationship between :class:`Signature` and :class:`FlippedSignature`, + described below. + """ + def __subclasscheck__(cls, subclass): + """ + Override of :pycode:`issubclass(cls, Signature)`. + + In addition to the standard behavior of :func:`issubclass`, this override makes + :class:`FlippedSignature` a subclass of :class:`Signature` or any of its subclasses. + """ + # `FlippedSignature` is a subclass of `Signature` or any of its subclasses because all of # them may return a Liskov-compatible instance of it from `self.flip()`. if subclass is FlippedSignature: @@ -335,6 +560,14 @@ def __subclasscheck__(cls, subclass): return super().__subclasscheck__(subclass) def __instancecheck__(cls, instance): + """ + Override of :pycode:`isinstance(obj, Signature)`. + + In addition to the standard behavior of :func:`isinstance`, this override makes + :pycode:`isinstance(obj, cls)` act as :pycode:`isinstance(obj.flip(), cls)` where + :pycode:`obj` is an instance of :class:`FlippedSignature`. + """ + # `FlippedSignature` is an instance of a `Signature` or its subclass if the unflipped # object is. if type(instance) is FlippedSignature: @@ -343,6 +576,9 @@ def __instancecheck__(cls, instance): class Signature(metaclass=SignatureMeta): + """ + .. todo:: Write this + """ def __init__(self, members): self.__members = SignatureMembers(members) @@ -468,6 +704,9 @@ def __repr__(self): # restriction could be lifted if there is a compelling use case. @final class FlippedSignature: + """ + .. todo:: Write this + """ def __init__(self, signature): object.__setattr__(self, "_FlippedSignature__unflipped", signature) @@ -513,6 +752,9 @@ def __repr__(self): class Interface: + """ + .. todo:: Write this + """ def __init__(self, signature, *, path): self.__dict__.update({ "signature": signature, @@ -524,6 +766,9 @@ def __init__(self, signature, *, path): # if there is a compelling use case. @final class FlippedInterface: + """ + .. todo:: Write this + """ def __init__(self, interface): if not (hasattr(interface, "signature") and isinstance(interface.signature, Signature)): raise TypeError(f"flipped() can only flip an interface object, not {interface!r}") @@ -552,6 +797,9 @@ def __repr__(self): def flipped(interface): + """ + .. todo:: Write this + """ if type(interface) is FlippedInterface: return interface._FlippedInterface__unflipped else: @@ -560,10 +808,17 @@ def flipped(interface): @final class ConnectionError(Exception): - pass + """ + This exception is raised when the :func:`connect` function is requested to perform + an impossible, meaningless, or forbidden connection. + """ def connect(m, *args, **kwargs): + """ + .. todo:: Write this + """ + objects = { **{index: arg for index, arg in enumerate(args)}, **{keyword: arg for keyword, arg in kwargs.items()} @@ -745,6 +1000,9 @@ def connect_dimensions(dimensions, *, out_path, in_path): class Component(Elaboratable): + """ + .. todo:: Write this + """ def __init__(self): for name in self.signature.members: if hasattr(self, name): diff --git a/docs/conf.py b/docs/conf.py index c92197fdef..a2d885fc58 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,3 +43,8 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] html_css_files = ["custom.css"] + +rst_prolog = """ +.. role:: pycode(code) + :language: python +""" diff --git a/docs/stdlib.rst b/docs/stdlib.rst index 48a793070a..6d084f6885 100644 --- a/docs/stdlib.rst +++ b/docs/stdlib.rst @@ -10,6 +10,7 @@ Standard library stdlib/enum stdlib/data + stdlib/wiring stdlib/coding stdlib/cdc stdlib/crc diff --git a/docs/stdlib/wiring.rst b/docs/stdlib/wiring.rst new file mode 100644 index 0000000000..368a44911e --- /dev/null +++ b/docs/stdlib/wiring.rst @@ -0,0 +1,102 @@ +.. + finished: 8/16 + + xx autoexception:: SignatureError + xx autoexception:: ConnectionError + xx autoclass:: Flow() + xx autodata:: Out + xx autodata:: In + xx autoclass:: Member(flow, description, *, reset=None) + .. autoclass:: SignatureMembers + xx autoclass:: FlippedSignatureMembers + .. autoclass:: Signature + .. autoclass:: FlippedSignature + xx autoclass:: SignatureMeta + .. autoclass:: Interface + .. autoclass:: FlippedInterface + .. autofunction:: flipped + .. autofunction:: connect + .. autoclass:: Component + +Components and interfaces +######################### + +.. py:module:: amaranth.lib.wiring + +The :mod:`amaranth.lib.wiring` module provides a way to describe the interfaces between components and connect them to each other in a reliable and convenient manner. + +.. testsetup:: + + from amaranth import * + from amaranth.lib import wiring + from amaranth.lib.wiring import In, Out + + +Introduction +============ + +.. todo:: + + Write this section + + +Signatures +========== + +.. autoclass:: Flow() + :no-members: + + .. autoattribute:: Out + :no-value: + .. autoattribute:: In + :no-value: + .. automethod:: flip + .. automethod:: __call__ + +.. autodata:: Out +.. autodata:: In + +.. autoclass:: Member(flow, description, *, reset=None) + +.. autoexception:: SignatureError + +.. autoclass:: SignatureMembers +.. autoclass:: FlippedSignatureMembers + +.. autoclass:: Signature +.. autoclass:: FlippedSignature +.. autoclass:: SignatureMeta + + +Interfaces +========== + +.. todo:: + + Finish this section + +.. autoclass:: Interface +.. autoclass:: FlippedInterface +.. autofunction:: flipped + + +Making connections +================== + +.. todo:: + + Finish this section + +.. autoexception:: ConnectionError + +.. autofunction:: connect + + +Components +========== + +.. todo:: + + Finish this section + +.. autoclass:: Component