diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py index a7bff3b..f54c076 100644 --- a/amaranth/lib/wiring.py +++ b/amaranth/lib/wiring.py @@ -1,7 +1,5 @@ from collections.abc import Mapping import enum -import types -import inspect import re import warnings @@ -15,10 +13,46 @@ __all__ = ["In", "Out", "Signature", "PureInterface", "connect", "flipped", "Com 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 = "Out" + + #: `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 = "In" def flip(self): + """Flip the direction of data flow. + + Returns + ------- + :class:`Flow` + :attr:`In` if called as :pc:`Out.flip()`; :attr:`Out` if called as :pc:`In.flip()`. + """ if self == Out: return In if self == In: @@ -26,6 +60,14 @@ class Flow(enum.Enum): assert False # :nocov: def __call__(self, description, *, reset=None): + """Create a :class:`Member` with this data flow and the provided description and + reset value. + + Returns + ------- + :class:`Member` + :pc:`Member(self, description, reset=reset)` + """ return Member(self, description, reset=reset) def __repr__(self): @@ -35,12 +77,34 @@ class Flow(enum.Enum): 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-like ` + object (in which case it is created as a port member). After creation the :class:`Member` + instance cannot be modified. + + When a :class:`Signal` is created from a description of a port member, the signal's reset value + is taken from the member description. If this signal is never explicitly assigned a value, it + will equal ``reset``. + + Although instances can be created directly, most often they will be created through + :data:`In` and :data:`Out`, e.g. :pc:`In(unsigned(1))` or :pc:`Out(stream.Signature(RGBPixel))`. + """ def __init__(self, flow, description, *, reset=None, _dimensions=()): self._flow = flow self._description = description @@ -75,10 +139,37 @@ class Member: 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 :pc:`member` with :pc:`member.flow` equal to :pc:`self.flow.flip()`, and identical + to :pc:`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, :pc:`Out(1).array(2)` describes an array of 2 elements, whereas both + :pc:`Out(1).array(2, 3)` and :pc:`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, :pc:`.array(x, y)` creates a member that can be indexed up to :pc:`[x-1][y-1]`. + + The :meth:`array` method is composable: calling :pc:`member.array(x)` describes an array of + :pc:`x` members even if :pc:`member` was already an array. + + Returns + ------- + :class:`Member` + A new :pc:`member` with :pc:`member.dimensions` extended by :pc:`dimensions`, and + identical to :pc:`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, " @@ -88,30 +179,88 @@ class Member: @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` + :pc:`True` if this is a description of a port member, + :pc:`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` + :pc:`True` if this is a description of a signature member, + :pc:`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-like object ` + The shape that was provided when constructing this :class:`Member`. + + Raises + ------ + :exc:`AttributeError` + If :pc:`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 :pc:`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 :pc:`self` describes a port member. + """ if self.is_port: raise AttributeError(f"A port member does not have a signature") if self.flow == Out: @@ -122,6 +271,16 @@ class Member: @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): @@ -142,13 +301,37 @@ class Member: @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): + """Mapping of signature member names to their descriptions. + + This container, a :class:`collections.abc.Mapping`, is used to implement the :pc:`members` + attribute of signature objects. + + The keys in this container must be valid Python attribute names that are public (do not begin + with an underscore. The values must be instances of :class:`Member`. The container is mutable + in a restricted manner: new keys may be added, but existing keys may not be modified or removed. + In addition, the container can be `frozen`, which disallows addition of new keys. Freezing + a container recursively freezes the members of any signatures inside. + + In addition to the use of the superscript operator, multiple members can be added at once with + the :pc:`+=` opreator. + + The :meth:`create` method converts this mapping into a mapping of names to signature members + (signals and interface objects) by creating them from their descriptions. The created mapping + can be used to populate an interface object. + """ + def __init__(self, members=()): self._dict = dict() for name, member in dict(members).items(): @@ -159,13 +342,34 @@ class SignatureMembers(Mapping): self._dict[name] = member def flip(self): + """Flip the data flow of the members in this mapping. + + Returns + ------- + :class:`FlippedSignatureMembers` + Proxy collection :pc:`FlippedSignatureMembers(self)` that flips the data flow of + the members that are accessed using it. + """ return FlippedSignatureMembers(self) def __eq__(self, other): + """Compare the members in this and another mapping. + + Returns + ------- + :class:`bool` + :pc:`True` if the mappings contain the same key-value pairs, :pc:`False` otherwise. + """ return (isinstance(other, (SignatureMembers, FlippedSignatureMembers)) and list(sorted(self.flatten())) == list(sorted(other.flatten()))) def __contains__(self, name): + """Check whether a member with a given name exists. + + Returns + ------- + :class:`bool` + """ return name in self._dict def _check_name(self, name): @@ -177,30 +381,112 @@ class SignatureMembers(Mapping): raise NameError(f"Member name cannot be '{name}'") def __getitem__(self, name): + """Retrieves the description of a member with a given name. + + Returns + ------- + :class:`Member` + + Raises + ------ + :exc:`TypeError` + If :pc:`name` is not a string. + :exc:`NameError` + If :pc:`name` is not a valid, public Python attribute name. + :exc:`SignatureError` + If a member called :pc:`name` does not exist in the collection. + """ self._check_name(name) if name not in self._dict: raise SignatureError(f"Member '{name}' is not a part of the signature") return self._dict[name] def __setitem__(self, name, member): + """Stub that forbids addition of members to the collection. + + Raises + ------ + :exc:`SignatureError` + Always. + """ raise SignatureError("Members cannot be added to a signature once constructed") def __delitem__(self, name): + """Stub that forbids removal of members from the collection. + + Raises + ------ + :exc:`SignatureError` + Always. + """ raise SignatureError("Members cannot be removed from a signature") def __iter__(self): + """Iterate through the names of members in the collection. + + Returns + ------- + iterator of :class:`str` + Names of members, in the order of insertion. + """ return iter(self._dict) def __len__(self): return len(self._dict) def flatten(self, *, path=()): + """Recursively iterate through this collection. + + .. note:: + + The :ref:`paths ` returned by this method and by :meth:`Signature.flatten` + differ. This method yields a single result for each :class:`Member` in the collection, + disregarding their dimensions: + + .. doctest:: + + >>> sig = wiring.Signature({ + ... "items": In(1).array(2) + ... }) + >>> list(sig.members.flatten()) + [(('items',), In(1).array(2))] + + The :meth:`Signature.flatten` method yields multiple results for such a member; see + the documentation for that method for an example. + + Returns + ------- + iterator of (:class:`tuple` of :class:`str`, :class:`Member`) + Pairs of :ref:`paths ` and the corresponding members. A path yielded by + this method is a tuple of strings where each item is a key through which the item may + be reached. + """ for name, member in self.items(): yield ((*path, name), member) if member.is_signature: yield from member.signature.members.flatten(path=(*path, name)) def create(self, *, path=None, src_loc_at=0): + """Create members from their descriptions. + + For each port member, this function creates a :class:`Signal` with the shape and reset + value taken from the member description, and the name constructed from + the :ref:`paths ` to the member (by concatenating path items with a double + underscore, ``__``). + + For each signature member, this function calls :meth:`Signature.create` for that signature. + The resulting object can have any type if a :class:`Signature` subclass overrides + the :class:`create` method. + + If the member description includes dimensions, in each case, instead of a single member, + a :class:`list` of members is created for each dimension. (That is, for a single dimension + a list of members is returned, for two dimensions a list of lists is returned, and so on.) + + Returns + ------- + dict of :class:`str` to :ref:`value-like ` or interface object or a potentially nested list of these + Mapping of names to actual signature members. + """ if path is None: path = (tracer.get_var_name(depth=2 + src_loc_at, default="$signature"),) attrs = {} @@ -229,10 +515,38 @@ class SignatureMembers(Mapping): @final class FlippedSignatureMembers(Mapping): + """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. For example: + + .. testcode:: + + members = wiring.SignatureMembers({"foo": Out(1)}) + + flipped_members = members.flip() + assert flipped_members["foo"].flow == In + + 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` + :pc:`unflipped` + """ return self.__unflipped # See the note below. @@ -300,7 +614,29 @@ def _format_shape(shape): class SignatureMeta(type): + """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 :pc:`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: @@ -308,6 +644,14 @@ class SignatureMeta(type): return super().__subclasscheck__(subclass) def __instancecheck__(cls, instance): + """ + Override of :pc:`isinstance(obj, Signature)`. + + In addition to the standard behavior of :func:`isinstance`, this override makes + :pc:`isinstance(obj, cls)` act as :pc:`isinstance(obj.flip(), cls)` where + :pc:`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: @@ -316,17 +660,69 @@ class SignatureMeta(type): class Signature(metaclass=SignatureMeta): + """Description of an interface object. + + An interface object is a Python object that has a :pc:`signature` attribute containing + a :class:`Signature` object, as well as an attribute for every member of its signature. + Signatures and interface objects are tightly linked: an interface object can be created out + of a signature, and the signature is used when :func:`connect` ing two interface objects + together. See the :ref:`introduction to interfaces ` for a more detailed + explanation of why this is useful. + + :class:`Signature` can be used as a base class to define :ref:`customized ` + signatures and interface objects. + + .. important:: + + :class:`Signature` objects are immutable. Classes inheriting from :class:`Signature` must + ensure this remains the case when additional functionality is added. + """ + def __init__(self, members): self.__members = SignatureMembers(members) def flip(self): + """Flip the data flow of the members in this signature. + + Returns + ------- + :class:`FlippedSignature` + Proxy object :pc:`FlippedSignature(self)` that flips the data flow of the attributes + corresponding to the members that are accessed using it. + + See the documentation for the :class:`FlippedSignature` class for a detailed discussion + of how this proxy object works. + """ return FlippedSignature(self) @property def members(self): + """Members in this signature. + + Returns + ------- + :class:`SignatureMembers` + """ return self.__members def __eq__(self, other): + """Compare this signature with another. + + The behavior of this operator depends on the types of the arguments. If both :pc:`self` + and :pc:`other` are instances of the base :class:`Signature` class, they are compared + structurally (the result is :pc:`self.members == other.members`); otherwise they are + compared by identity (the result is :pc:`self is other`). + + Subclasses of :class:`Signature` are expected to override this method to take into account + the specifics of the domain. If the subclass has additional properties that do not influence + the :attr:`members` dictionary but nevertheless make its instance incompatible with other + instances (for example, whether the feedback is combinational or registered), + the overridden method must take that into account. + + Returns + ------- + :class:`bool` + """ other_unflipped = other.flip() if type(other) is FlippedSignature else other if type(self) is type(other_unflipped) is Signature: # If both `self` and `other` are anonymous signatures, compare structurally. @@ -337,6 +733,36 @@ class Signature(metaclass=SignatureMeta): return self is other def flatten(self, obj): + """Recursively iterate through this signature, retrieving member values from an interface + object. + + .. note:: + + The :ref:`paths ` returned by this method and by + :meth:`SignatureMembers.flatten` differ. This method yield several results for each + :class:`Member` in the collection that has a dimension: + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> sig = wiring.Signature({ + ... "items": In(1).array(2) + ... }) + >>> obj = sig.create() + >>> list(sig.flatten(obj)) + [(('items', 0), In(1), (sig obj__items__0)), + (('items', 1), In(1), (sig obj__items__1))] + + The :meth:`SignatureMembers.flatten` method yields one result for such a member; see + the documentation for that method for an example. + + Returns + ------- + iterator of (:class:`tuple` of :class:`str` or :class:`int`, :class:`Flow`, :ref:`value-like `) + Tuples of :ref:`paths `, flow, and the corresponding member values. A path + yielded by this method is a tuple of strings or integers where each item is an attribute + name or index (correspondingly) using which the member value was retrieved. + """ for name, member in self.members.items(): path = (name,) value = getattr(obj, name) @@ -364,6 +790,51 @@ class Signature(metaclass=SignatureMeta): yield from iter_dimensions(value, dimensions=member.dimensions, path=path) def is_compliant(self, obj, *, reasons=None, path=("obj",)): + """Check whether an object matches the description in this signature. + + This module places few restrictions on what an interface object may be; it does not + prescribe a specific base class or a specific way of constructing the object, only + the values that its attributes should have. This method ensures consistency between + the signature and the interface object, checking every aspect of the provided interface + object for compliance with the signature. + + It verifies that: + + * :pc:`obj` has a :pc:`signature` attribute whose value a :class:`Signature` instance + such that ``self == obj.signature``; + * for each member, :pc:`obj` has an attribute with the same name, whose value: + + * for members with :meth:`dimensions ` specified, contains a list or + a tuple (or several levels of nested lists or tuples, for multiple dimensions) + satisfying the requirements below; + * for port members, is a :ref:`value-like ` object casting to + a :class:`Signal` or a :class:`Const` whose width and signedness is the same as that + of the member, and (in case of a :class:`Signal`) which is not reset-less and whose + reset value is that of the member; + * for signature members, matches the description in the signature as verified by + :meth:`Signature.is_compliant`. + + If the verification fails, this method reports the reason(s) by filling the :pc:`reasons` + container. These reasons are intended to be human-readable: more than one reason may be + reported but only in cases where this is helpful (e.g. the same error message will not + repeat 10 times for each of the 10 ports in a list). + + Arguments + --------- + reasons : :class:`list` or :pc:`None` + If provided, a container that receives diagnostic messages. + path : :class:`tuple` of :class:`str` + The :ref:`path ` to :pc:`obj`. Could be set to improve diagnostic + messages if :pc:`obj` is nested within another object, or for clarity. + + Returns + ------- + :class:`bool` + :pc:`True` if :pc:`obj` matches the description in this signature, :pc:`False` + otherwise. If :pc:`False` and :pc:`reasons` was not :pc:`None`, it will contain + a detailed explanation why. + """ + if not hasattr(obj, "signature"): if reasons is not None: reasons.append(f"{_format_path(path)} does not have an attribute 'signature'") @@ -463,6 +934,43 @@ class Signature(metaclass=SignatureMeta): return result def create(self, *, path=None, src_loc_at=0): + """Create an interface object from this signature. + + The default :meth:`Signature.create` implementation consists of one line: + + .. code:: + + def create(self, *, path=None, src_loc_at=0): + return PureInterface(self, path=path, src_loc_at=1 + src_loc_at) + + This implementation creates an interface object from this signature that serves purely + as a container for the attributes corresponding to the signature members, and implements + no behavior. Such an implementation is sufficient for signatures created ad-hoc using + the :pc:`Signature({ ... })` constructor as well as simple signature subclasses. + + When defining a :class:`Signature` subclass that needs to customize the behavior of + the created interface objects, override this method with a similar implementation + that references the class of your custom interface object: + + .. testcode:: + + class CustomSignature(wiring.Signature): + def create(self, *, path=None, src_loc_at=0): + return CustomInterface(self, path=path, src_loc_at=1 + src_loc_at) + + class CustomInterface(wiring.PureInterface): + @property + def my_property(self): + ... + + The :pc:`path` and :pc:`src_loc_at` arguments are necessary to ensure the generated signals + have informative names and accurate source location information. + + The custom :meth:`create` method may take positional or keyword arguments in addition to + the two listed above. Such arguments must have a default value, because + the :meth:`SignatureMembers.create` method will call the :meth:`Signature.create` member + without these additional arguments when this signature is a member of another signature. + """ return PureInterface(self, path=path, src_loc_at=1 + src_loc_at) def __repr__(self): @@ -479,7 +987,7 @@ def _gettypeattr(obj, attr): return cls.__dict__[attr] except KeyError: pass - # Call `getattr` In case there is `__getattr__` on the metaclass, or just to generate + # Call `getattr` in case there is `__getattr__` on the metaclass, or just to generate # an `AttributeError` with the standard message. return getattr(type(obj), attr) @@ -488,12 +996,67 @@ def _gettypeattr(obj, attr): # restriction could be lifted if there is a compelling use case. @final class FlippedSignature: + """Description of an interface object, with the members' directions flipped. + + Although an instance of :class:`FlippedSignature` could be created directly, it will be usually + created by a call to :meth:`Signature.flip`. + + This proxy is a wrapper around :class:`Signature` that contains the same description as + the inner mapping, but flips the members' data flow when they are accessed. It is useful + because :class:`Signature` objects are mutable and may include custom behavior, and if one was + copied (rather than wrapped) by :meth:`Signature.flip`, the wrong object would be mutated, and + custom behavior would be unavailable. + + For example: + + .. testcode:: + + sig = wiring.Signature({"foo": Out(1)}) + + flipped_sig = sig.flip() + assert flipped_sig.members["foo"].flow == In + + sig.attr = 1 + assert flipped_sig.attr == 1 + flipped_sig.attr += 1 + assert sig.attr == flipped_sig.attr == 2 + + This class implements the same methods, with the same functionality (other than the flipping of + the members' data flow), as the :class:`Signature` class; see the documentation for that class + for details. + + It is not possible to inherit from :class:`FlippedSignature` and :meth:`Signature.flip` must not + be overridden. If a :class:`Signature` subclass defines a method and this method is called on + a flipped instance of the subclass, it receives the flipped instance as its :pc:`self` argument. + To distinguish being called on the flipped instance from being called on the unflipped one, use + :pc:`isinstance(self, FlippedSignature)`: + + .. testcode:: + + class SignatureKnowsWhenFlipped(wiring.Signature): + @property + def is_flipped(self): + return isinstance(self, wiring.FlippedSignature) + + sig = SignatureKnowsWhenFlipped({}) + assert sig.is_flipped == False + assert sig.flip().is_flipped == True + """ def __init__(self, signature): object.__setattr__(self, "_FlippedSignature__unflipped", signature) def flip(self): + """ + Flips this signature back to the original one. + + Returns + ------- + :class:`Signature` + :pc:`unflipped` + """ return self.__unflipped + # Flipped version of :meth:`Signature.members`. Documented only on :class:`Signature`. @property def members(self): return FlippedSignatureMembers(self.__unflipped.members) @@ -514,31 +1077,46 @@ class FlippedSignature: # between the normal and the flipped member collections. is_compliant = Signature.is_compliant - # FIXME: document this logic - # Because we would like to forward attribute access (other than what is explicitly overridden) # to the unflipped signature, including access via e.g. @property-decorated functions, we have # to reimplement the Python decorator protocol here. Note that in all of these functions, there # are two possible exits via `except AttributeError`: from `getattr` and from `.__get__()`. def __getattr__(self, name): + """Retrieves attribute or method :pc:`name` of the unflipped signature. + + Performs :pc:`getattr(unflipped, name)`, ensuring that, if :pc:`name` refers to a property + getter or a method, its :pc:`self` argument receives the *flipped* signature. A class + method's :pc:`cls` argument receives the class of the *unflipped* signature, as usual. + """ try: # descriptor first return _gettypeattr(self.__unflipped, name).__get__(self, type(self.__unflipped)) except AttributeError: return getattr(self.__unflipped, name) def __setattr__(self, name, value): + """Assigns attribute :pc:`name` of the unflipped signature to ``value``. + + Performs :pc:`setattr(unflipped, name, value)`, ensuring that, if :pc:`name` refers to + a property setter, its :pc:`self` argument receives the flipped signature. + """ try: # descriptor first _gettypeattr(self.__unflipped, name).__set__(self, value) except AttributeError: setattr(self.__unflipped, name, value) def __delattr__(self, name): + """Removes attribute :pc:`name` of the unflipped signature. + + Performs :pc:`delattr(unflipped, name)`, ensuring that, if :pc:`name` refers to a property + deleter, its :pc:`self` argument receives the flipped signature. + """ try: # descriptor first _gettypeattr(self.__unflipped, name).__delete__(self) except AttributeError: delattr(self.__unflipped, name) + # Flipped version of :meth:`Signature.create`. Documented only on :class:`Signature`. def create(self, *args, path=None, src_loc_at=0, **kwargs): return flipped(self.__unflipped.create(*args, path=path, src_loc_at=1 + src_loc_at, **kwargs)) @@ -548,7 +1126,42 @@ class FlippedSignature: class PureInterface: + """A helper for constructing ad-hoc interfaces. + + The :class:`PureInterface` helper primarily exists to be used by the default implementation of + :meth:`Signature.create`, but it can also be used in any other context where an interface + object needs to be created without the overhead of defining a class for it. + + .. important:: + + Any object can be an interface object; it only needs a :pc:`signature` property containing + a compliant signature. It is **not** necessary to use :class:`PureInterface` in order to + create an interface object, but it may be used either directly or as a base class whenever + it is convenient to do so. + """ + def __init__(self, signature, *, path=None, src_loc_at=0): + """Create attributes from a signature. + + The sole method defined by this helper is its constructor, which only defines + the :pc:`self.signature` attribute as well as the attributes created from the signature + members: + + .. code:: + + def __init__(self, signature, *, path): + self.__dict__.update({ + "signature": signature, + **signature.members.create(path=path) + }) + + .. note:: + + This implementation can be copied and reused in interface objects that *do* include + custom behavior, if the signature serves as the source of truth for attributes + corresponding to its members. Although it is less repetitive, this approach can confuse + IDEs and type checkers. + """ self.__dict__.update({ "signature": signature, **signature.members.create(path=path, src_loc_at=1 + src_loc_at) @@ -565,6 +1178,50 @@ class PureInterface: # if there is a compelling use case. @final class FlippedInterface: + """An interface object, with its members' directions flipped. + + An instance of :class:`FlippedInterface` should only be created by calling :func:`flipped`, + which ensures that a :pc:`FlippedInterface(FlippedInterface(...))` object is never created. + + This proxy wraps any interface object and forwards attribute and method access to the wrapped + interface object while flipping its signature and the values of any attributes corresponding to + interface members. It is useful because interface objects may be mutable or include custom + behavior, and explicitly keeping track of whether the interface object is flipped would be very + burdensome. + + For example: + + .. testcode:: + + intf = wiring.PureInterface(wiring.Signature({"foo": Out(1)}), path=()) + + flipped_intf = wiring.flipped(intf) + assert flipped_intf.signature.members["foo"].flow == In + + intf.attr = 1 + assert flipped_intf.attr == 1 + flipped_intf.attr += 1 + assert intf.attr == flipped_intf.attr == 2 + + It is not possible to inherit from :class:`FlippedInterface`. If an interface object class + defines a method or a property and it is called on the flipped interface object, the method + receives the flipped interface object as its :pc:`self` argument. To distinguish being called + on the flipped interface object from being called on the unflipped one, use + :pc:`isinstance(self, FlippedInterface)`: + + .. testcode:: + + class InterfaceKnowsWhenFlipped: + signature = wiring.Signature({}) + + @property + def is_flipped(self): + return isinstance(self, wiring.FlippedInterface) + + intf = InterfaceKnowsWhenFlipped() + assert intf.is_flipped == False + assert wiring.flipped(intf).is_flipped == True + """ 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}") @@ -572,17 +1229,39 @@ class FlippedInterface: @property def signature(self): + """Signature of the flipped interface. + + Returns + ------- + Signature + :pc:`unflipped.signature.flip()` + """ return self.__unflipped.signature.flip() def __eq__(self, other): - return type(self) is type(other) and self.__unflipped == other.__unflipped + """Compare this flipped interface with another. - # FIXME: document this logic + Returns + ------- + bool + :pc:`True` if :pc:`other` is an instance :pc:`FlippedInterface(other_unflipped)` where + :pc:`unflipped == other_unflipped`, :pc:`False` otherwise. + """ + return type(self) is type(other) and self.__unflipped == other.__unflipped # See the note in `FlippedSignature`. In addition, these accessors also handle flipping of # an interface member. def __getattr__(self, name): + """Retrieves attribute or method :pc:`name` of the unflipped interface. + + Performs :pc:`getattr(unflipped, name)`, with the following caveats: + + 1. If :pc:`name` refers to a signature member, the returned interface object is flipped. + 2. If :pc:`name` refers to a property getter or a method, its :pc:`self` argument receives + the *flipped* interface. A class method's :pc:`cls` argument receives the class of + the *unflipped* interface, as usual. + """ if (name in self.__unflipped.signature.members and self.__unflipped.signature.members[name].is_signature): return flipped(getattr(self.__unflipped, name)) @@ -593,6 +1272,14 @@ class FlippedInterface: return getattr(self.__unflipped, name) def __setattr__(self, name, value): + """Assigns attribute :pc:`name` of the unflipped interface to ``value``. + + Performs :pc:`setattr(unflipped, name, value)`, with the following caveats: + + 1. If :pc:`name` refers to a signature member, the assigned interface object is flipped. + 2. If :pc:`name` refers to a property setter, its :pc:`self` argument receives the flipped + interface. + """ if (name in self.__unflipped.signature.members and self.__unflipped.signature.members[name].is_signature): setattr(self.__unflipped, name, flipped(value)) @@ -603,6 +1290,11 @@ class FlippedInterface: setattr(self.__unflipped, name, value) def __delattr__(self, name): + """Removes attribute :pc:`name` of the unflipped interface. + + Performs :pc:`delattr(unflipped, name)`, ensuring that, if :pc:`name` refers to a property + deleter, its :pc:`self` argument receives the flipped interface. + """ try: # descriptor first _gettypeattr(self.__unflipped, name).__delete__(self) except AttributeError: @@ -613,6 +1305,16 @@ class FlippedInterface: def flipped(interface): + """ + Flip the data flow of the members of the interface object :pc:`interface`. + + If an interface object is flipped twice, returns the original object: + :pc:`flipped(flipped(interface)) is interface`. Otherwise, wraps :pc:`interface` in + a :class:`FlippedInterface` proxy object that flips the directions of its members. + + See the documentation for the :class:`FlippedInterface` class for a detailed discussion of how + this proxy object works. + """ if type(interface) is FlippedInterface: return interface._FlippedInterface__unflipped else: @@ -621,10 +1323,56 @@ def flipped(interface): @final class ConnectionError(Exception): - pass + """Exception raised when the :func:`connect` function is requested to perform an impossible, + meaningless, or forbidden connection.""" def connect(m, *args, **kwargs): + """Connect interface objects to each other. + + This function creates connections between ports of several interface objects. (Any number of + interface objects may be provided; in most cases it is two.) + + The connections can be made only if all of the objects satisfy a number of requirements: + + * Every interface object must have the same set of port members, and they must have the same + :meth:`dimensions `. + * For each path, the port members of every interface object must have the same width and reset + value (for port members corresponding to signals) or constant value (for port members + corresponding to constants). Signedness may differ. + * For each path, at most one interface object must have the corresponding port member be + an output. + * For a given path, if any of the interface objects has an input port member corresponding + to a constant value, then the rest of the interface objects must have output port members + corresponding to the same constant value. + + For example, if :pc:`obj1` is being connected to :pc:`obj2` and :pc:`obj3`, and :pc:`obj1.a.b` + is an output, then :pc:`obj2.a.b` and :pc:`obj2.a.b` must exist and be inputs. If :pc:`obj2.c` + is an input and its value is :pc:`Const(1)`, then :pc:`obj1.c` and :pc:`obj3.c` must be outputs + whose value is also :pc:`Const(1)`. If no ports besides :pc:`obj1.a.b` and :pc:`obj1.c` exist, + then no ports except for those two must exist on :pc:`obj2` and :pc:`obj3` either. + + Once it is determined that the interface objects can be connected, this function performs + an equivalent of: + + .. code:: + + m.d.comb += [ + in1.eq(out1), + in2.eq(out1), + ... + ] + + Where :pc:`out1` is an output and :pc:`in1`, :pc:`in2`, ... are the inputs that have the same + path. (If no interface object has an output for a given path, **no connection at all** is made.) + + The positions (within :pc:`args`) or names (within :pc:`kwargs`) of the arguments do not affect + the connections that are made. There is no difference in behavior between :pc:`connect(m, a, b)` + and :pc:`connect(m, b, a)` or :pc:`connect(m, arbiter=a, decoder=b)`. The names of the keyword + arguments serve only a documentation purpose: they clarify the diagnostic messages when + a connection cannot be made. + """ + objects = { **{index: arg for index, arg in enumerate(args)}, **{keyword: arg for keyword, arg in kwargs.items()} @@ -774,15 +1522,16 @@ def connect(m, *args, **kwargs): # If the output is not a constant, the connection is illegal. if type(out_value) is not Const: raise ConnectionError( - f"Cannot connect to the input member {_format_path(in_path)} that has " - f"a constant value {in_value.value!r}") + f"Cannot connect input member {_format_path(in_path)} that has " + f"a constant value {in_value.value!r} to an output member " + f"{_format_path(out_path)} that has a varying value") # If the output is a constant, the connection is legal only if the value is # the same for both the input and the output. if type(out_value) is Const and in_value.value != out_value.value: raise ConnectionError( f"Cannot connect input member {_format_path(in_path)} that has " f"a constant value {in_value.value!r} to an output member " - f"{_format_path(out_path)} that has a differing constant value " + f"{_format_path(out_path)} that has a different constant value " f"{out_value.value!r}") # We never actually connect anything to the constant input; we only ensure its # value (which is constant) is consistent with a connection that would have @@ -806,6 +1555,61 @@ def connect(m, *args, **kwargs): class Component(Elaboratable): + """Base class for elaboratable interface objects. + + A component is an :class:`Elaboratable` whose interaction with other parts of the design is + defined by its signature. Most if not all elaboratables in idiomatic Amaranth code should be + components, as the signature clarifies the direction of data flow at their boundary. See + the :ref:`introduction to interfaces ` section for a practical guide to defining + and using components. + + There are two ways to define a component. If all instances of a component have the same + signature, it can be defined using :term:`variable annotations `: + + .. testcode:: + + class FixedComponent(wiring.Component): + en: In(1) + data: Out(8) + + The variable annotations are collected by the constructor :meth:`Component.__init__`. Only + public (not starting with ``_``) annotations with :class:`In ` or :class:`Out ` + objects are considered; all other annotations are ignored under the assumption that they are + interpreted by some other tool. + + It is possible to use inheritance to extend a component: the component's signature is composed + from the variable annotations in the class that is being constructed as well as all of its + base classes. It is an error to have more than one variable annotation for the same attribute. + + If different instances of a component may need to have different signatures, variable + annotations cannot be used. In this case, the constructor should be overridden, and + the computed signature members should be provided to the superclass constructor: + + .. testcode:: + + class ParametricComponent(wiring.Component): + def __init__(self, data_width): + super().__init__({ + "en": In(1), + "data": Out(data_width) + }) + + It is also possible to pass a :class:`Signature` instance to the superclass constructor. + + Aside from initializing the :attr:`signature` attribute, the :meth:`Component.__init__` + constructor creates attributes corresponding to all of the members defined in the signature. + If an attribute with the same name as that of a member already exists, an error is raied. + + Raises + ------ + :exc:`TypeError` + If the :pc:`signature` object is neither a :class:`Signature` nor a :class:`dict`. + If neither variable annotations nor the :pc:`signature` argument are present, or if + both are present. + :exc:`NameError` + If a name conflict is detected between two variable annotations, or between a member + and an existing attribute. + """ def __init__(self, signature=None): cls = type(self) members = {} @@ -815,11 +1619,12 @@ class Component(Elaboratable): continue if type(annot) is Member: if name in members: - raise SignatureError(f"Member '{name}' is redefined in {base.__module__}.{base.__qualname__}") + raise NameError( + f"Member '{name}' is redefined in {base.__module__}.{base.__qualname__}") members[name] = annot if not members: if signature is None: - raise NotImplementedError( + raise TypeError( f"Component '{cls.__module__}.{cls.__qualname__}' does not have signature " f"member annotations") if isinstance(signature, dict): @@ -843,4 +1648,12 @@ class Component(Elaboratable): @property def signature(self): + """The signature of the component. + + .. important:: + + Do not override this property. Once a component is constructed, its :attr:`signature` + property must always return the same :class:`Signature` instance. The constructor + can be used to customize a component's signature. + """ return self.__signature diff --git a/docs/conf.py b/docs/conf.py index c92197f..8487b09 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,3 +43,8 @@ napoleon_custom_sections = ["Platform overrides"] html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] html_css_files = ["custom.css"] + +rst_prolog = """ +.. role:: pc(code) + :language: python +""" diff --git a/docs/stdlib.rst b/docs/stdlib.rst index 48a7930..6d084f6 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/data.rst b/docs/stdlib/data.rst index 4590b23..0161790 100644 --- a/docs/stdlib/data.rst +++ b/docs/stdlib/data.rst @@ -20,6 +20,12 @@ This module provides four related facilities: 3. Data views via :class:`View` or its user-defined subclasses. This class is used to apply a layout description to a plain :class:`Value`, enabling structured access to its bits. 4. Data classes :class:`Struct` and :class:`Union`. These classes are data views with a layout that is defined using Python :term:`variable annotations ` (also known as type annotations). +To use this module, add the following imports to the beginning of the file: + +.. testcode:: + + from amaranth.lib import data + Motivation ++++++++++ diff --git a/docs/stdlib/wiring.rst b/docs/stdlib/wiring.rst new file mode 100644 index 0000000..01d5c00 --- /dev/null +++ b/docs/stdlib/wiring.rst @@ -0,0 +1,600 @@ +Interfaces and connections +########################## + +.. py:module:: amaranth.lib.wiring + +The :mod:`amaranth.lib.wiring` module provides a way to declare the interfaces between design components and connect them to each other in a reliable and convenient way. + +.. testsetup:: + + from amaranth import * + + +.. _wiring-introduction: + +Introduction +============ + +Overview +++++++++ + +This module provides four related facilities: + +1. Description and construction of interface objects via :class:`Flow` (:data:`In` and :data:`Out`), :class:`Member`, and :class:`Signature`, as well as the associated container class :class:`SignatureMembers`. These classes provide the syntax used in defining components, and are also useful for introspection. +2. Flipping of signatures and interface objects via :class:`FlippedSignature` and :class:`FlippedInterface`, as well as the associated container class :class:`FlippedSignatureMembers`. This facility reduces boilerplate by adapting existing signatures and interface objects: the flip operation changes the :data:`In` data flow of a member to :data:`Out` and vice versa. +3. Connecting interface objects together via :func:`connect`. The :func:`connect` function ensures that the provided interface objects can be connected to each other, and adds the necessary :pc:`.eq()` statements to a :class:`Module`. +4. Defining reusable, self-contained components via :class:`Component`. Components are :class:`Elaboratable` objects that interact with the rest of the design through an interface specified by their signature. + +To use this module, add the following imports to the beginning of the file: + +.. testcode:: + + from amaranth.lib import wiring + from amaranth.lib.wiring import In, Out + +The :ref:`"Motivation" ` and :ref:`"Reusable interfaces" ` sections describe concepts that are essential for using this module and writing idiomatic Amaranth code. The sections after describe advanced use cases that are only relevant for more complex code. + + +.. _wiring-intro1: + +Motivation +++++++++++ + +Consider a reusable counter with an enable input, configurable limit, and an overflow flag. Using only the core Amaranth language, it could be implemented as: + +.. testcode:: + + class BasicCounter(Elaboratable): + def __init__(self): + self.en = Signal() + + self.count = Signal(8) + self.limit = Signal.like(self.count) + + self.overflow = Signal() + + def elaborate(self, platform): + m = Module() + + with m.If(self.en): + m.d.sync += self.overflow.eq(0) + with m.If(self.count == self.limit): + m.d.sync += self.overflow.eq(1) + m.d.sync += self.count.eq(0) + with m.Else(): + m.d.sync += self.count.eq(self.count + 1) + + return m + +Nothing in this implementation indicates the directions of its ports (:pc:`en`, :pc:`count`, :pc:`limit`, and :pc:`overflow`) in relation to other parts of the design. To understand whether the value of a port is expected to be provided externally or generated internally, it is first necessary to read the body of the :pc:`elaborate` method. If the port is not used within that method in a particular elaboratable, it is not possible to determine its direction, or whether it is even meant to be connected. + +The :mod:`amaranth.lib.wiring` module provides a solution for this problem: *components*. A component is an elaboratable that declares the shapes and directions of its ports in its *signature*. The example above can be rewritten to use the :class:`Component` base class (which itself inherits from :class:`Elaboratable`) to be: + +.. testcode:: + + class ComponentCounter(wiring.Component): + en: In(1) + + count: Out(8) + limit: In(8) + + overflow: Out(1) + + def elaborate(self, platform): + m = Module() + + with m.If(self.en): + m.d.sync += self.overflow.eq(0) + with m.If(self.count == self.limit): + m.d.sync += self.overflow.eq(1) + m.d.sync += self.count.eq(0) + with m.Else(): + m.d.sync += self.count.eq(self.count + 1) + + return m + +The code in the constructor *creating* the signals of the counter's interface one by one is now gone, replaced with the :term:`variable annotations ` *declaring* the counter's interface. The inherited constructor, :meth:`Component.__init__`, creates the same attributes with the same values as before, and the :pc:`elaborate` method is unchanged. + +The major difference between the two examples is that the :pc:`ComponentCounter` provides unambiguous answers to two questions that previously required examining the :pc:`elaborate` method: + +1. Which of the Python object's attributes are ports that are intended to be connected to the rest of the design. +2. What is the direction of the flow of information through the port. + +This information, aside from being clear from the source code, can now be retrieved from the :pc:`.signature` attribute, which contains an instance of the :class:`Signature` class: + +.. doctest:: + + >>> ComponentCounter().signature + Signature({'en': In(1), 'count': Out(8), 'limit': In(8), 'overflow': Out(1)}) + +The :ref:`shapes ` of the ports need not be static. The :pc:`ComponentCounter` can be made generic, with its range specified when it is constructed, by creating the signature explicitly in its constructor: + +.. testcode:: + + class GenericCounter(wiring.Component): + def __init__(self, width): + super().__init__({ + "en": In(1), + + "count": Out(width), + "limit": In(width), + + "overflow": Out(1) + }) + + # The implementation of the `elaborate` method is the same. + elaborate = ComponentCounter.elaborate + +.. doctest:: + + >>> GenericCounter(16).signature + Signature({'en': In(1), 'count': Out(16), 'limit': In(16), 'overflow': Out(1)}) + +Instances of the :class:`ComponentCounter` and :class:`GenericCounter` class are two examples of *interface objects*. An interface object is a Python object of any type whose a :pc:`signature` attribute contains a :class:`Signature` with which the interface object is compliant (as determined by the :meth:`is_compliant ` method of the signature). + +The next section introduces the concepts of directionality and connection, and discusses interface objects in more detail. + + +.. _wiring-intro2: + +Reusable interfaces ++++++++++++++++++++ + +Consider a more complex example where two components are communicating with a *stream* that is using *ready/valid signaling*, where the :pc:`valid` signal indicates that the value of :pc:`data` provided by the source is meaningful, and the :pc:`ready` signal indicates that the sink has consumed the data word: + +.. testcode:: + + class DataProducer(wiring.Component): + en: In(1) + + data: Out(8) + valid: Out(1) + ready: In(1) + + def elaborate(self, platform): ... + + + class DataConsumer(wiring.Component): + data: In(8) + valid: In(1) + ready: Out(1) + + # ... other ports... + + def elaborate(self, platform): ... + +Data would be transferred between these components by assigning the outputs to the inputs elsewhere in the design: + +.. testcode:: + + m = Module() + m.submodules.producer = producer = DataProducer() + m.submodules.consumer = consumer = DataConsumer() + + ... + + m.d.comb += [ + consumer.data.eq(producer.data), + consumer.valid.eq(producer.valid), + producer.ready.eq(consumer.ready), + ] + +Although this example is short, it is already repetitive and redundant. The ports on the producer and the consumer, which must match each other for the connection to be made correctly, are declared twice; and the connection itself is made in an error-prone manual way even though the signatures include all of the information required to create it. + +The signature of a stream could be defined in a generic way: + +.. testcode:: + + class SimpleStreamSignature(wiring.Signature): + def __init__(self, data_shape): + super().__init__({ + "data": Out(data_shape), + "valid": Out(1), + "ready": In(1) + }) + + def __eq__(self, other): + return self.members == other.members + +.. doctest:: + + >>> SimpleStreamSignature(8).members + SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)}) + +A definition like this is usable, depending on the data flow direction of the members, only in the producer (as in the code above) or only in the consumer. To resolve this problem, this module introduces *flipping*: an operation that reverses the data flow direction of the members of a signature or an interface object while leaving everything else about the object intact. In Amaranth, the (non-flipped) signature definition always declares the data flow directions appropriate for a bus initiator, stream source, controller, and so on. A bus target, stream sink, peripheral, and so on would reuse the source definition by flipping it. + +A signature is flipped by calling :meth:`sig.flip() `, and an interface object is flipped by calling :func:`flipped(intf) `. These calls return instances of the :class:`FlippedSignature` and :class:`FlippedInterface` classes, respectively, which use metaprogramming to wrap another object, changing only the data flow directions of its members and forwarding all other method calls and attribute accesses to the wrapped object. + +The example above can be rewritten to use this definition of a stream signature as: + +.. testcode:: + + class StreamProducer(wiring.Component): + en: In(1) + source: Out(SimpleStreamSignature(8)) + + def elaborate(self, platform): ... + + + class StreamConsumer(wiring.Component): + sink: Out(SimpleStreamSignature(8).flip()) + + def elaborate(self, platform): ... + + + m = Module() + m.submodules.producer = producer = StreamProducer() + m.submodules.consumer = consumer = StreamConsumer() + +The producer and the consumer reuse the same signature, relying on flipping to make the port directions complementary: + +.. doctest:: + + >>> producer.source.signature.members + SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)}) + >>> producer.source.signature.members['data'] + Out(8) + >>> consumer.sink.signature.members + SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)}).flip() + >>> consumer.sink.signature.members['data'] + In(8) + +In the :pc:`StreamConsumer` definition above, the :pc:`sink` member has its direction flipped explicitly because the sink is a stream input; this is the case for every interface due to how port directions are defined. Since this operation is so ubiquitous, it is also performed when :pc:`In(...)` is used with a signature rather than a shape. The :pc:`StreamConsumer` definition above should normally be written as: + +.. testcode:: + + class StreamConsumerUsingIn(wiring.Component): + sink: In(SimpleStreamSignature(8)) + + def elaborate(self, platform): ... + +The data flow directions of the ports are identical between the two definitions: + +.. doctest:: + + >>> consumer.sink.signature.members == StreamConsumerUsingIn().sink.signature.members + True + +If signatures are nested within each other multiple levels deep, the final port direction is determined by how many nested :pc:`In(...)` members there are. For each :pc:`In(...)` signature wrapping a port, the data flow direction of the port is flipped once: + +.. doctest:: + + >>> sig = wiring.Signature({"port": Out(1)}) + >>> sig.members["port"] + Out(1) + >>> in1 = wiring.Signature({"sig": In(sig)}) + >>> in1.members["sig"].signature.members["port"] + In(1) + >>> in2 = wiring.Signature({"sig": In(in1)}) + >>> in2.members["sig"].signature.members["sig"].signature.members["port"] + Out(1) + +Going back to the stream example, the producer and the consumer now communicate with one another using the same set of ports with identical shapes and complementary directions (the auxiliary :pc:`en` port being outside of the stream signature), and can be *connected* using the :func:`connect` function: + +.. testcode:: + + wiring.connect(m, producer.source, consumer.sink) + +This function examines the signatures of the two provided interface objects, ensuring that they are exactly complementary, and then adds combinatorial :pc:`.eq()` statements to the module for each of the port pairs to form the connection. Aside from the *connectability* check, the single line above is equivalent to: + +.. testcode:: + + m.d.comb += [ + consumer.sink.data.eq(producer.source.data), + consumer.sink.valid.eq(producer.source.valid), + producer.source.ready.eq(consumer.sink.ready), + ] + +Even on the simple example of a stream signature it is clear how using the :func:`connect` function results in more concise, readable, and robust code. The difference is proportionally more pronounced with more complex signatures. When a signature is being refactored, no changes to the code that uses :func:`connect` is required. + +This explanation concludes the essential knowledge necessary for using this module and writing idiomatic Amaranth code. + + +.. _wiring-forwarding: + +Forwarding interior interfaces +++++++++++++++++++++++++++++++ + +Consider a case where a component includes another component as a part of its implementation, and where it is necessary to *forward* the ports of the inner component, that is, expose them within the outer component's signature. To use the :pc:`SimpleStreamSignature` definition above in an example: + +.. testcode:: + + class DataProcessorImplementation(wiring.Component): + source: Out(SimpleStreamSignature(8)) + + def elaborate(self, platform): ... + + + class DataProcessorWrapper(wiring.Component): + source: Out(SimpleStreamSignature(8)) + + def elaborate(self, platform): + m = Module() + m.submodules.impl = impl = DataProcessorImplementation() + m.d.comb += [ + self.source.data.eq(impl.source.data), + self.source.valid.eq(impl.source.valid), + impl.source.ready.eq(self.source.ready), + ] + return m + +Because forwarding the ports requires assigning an output to an output and an input to an input, the :func:`connect` function, which connects outputs to inputs and vice versa, cannot be used---at least not directly. The :func:`connect` function is designed to cover the usual case of connecting the interfaces of modules *from outside* those modules. In order to connect an interface *from inside* a module, it is necessary to flip that interface first using the :func:`flipped` function. The :pc:`DataProcessorWrapper` should instead be implemented as: + +.. testcode:: + + class DataProcessorWrapper(wiring.Component): + source: Out(SimpleStreamSignature(8)) + + def elaborate(self, platform): + m = Module() + m.submodules.impl = impl = DataProcessorImplementation() + wiring.connect(m, wiring.flipped(self.source), impl.source) + return m + +In some cases, *both* of the two interfaces provided to :func:`connect` must be flipped. For example, the correct way to implement a component that forwards an input interface to an output interface with no processing is: + +.. testcode:: + + class DataForwarder(wiring.Component): + sink: In(SimpleStreamSignature(8)) + source: Out(SimpleStreamSignature(8)) + + def elaborate(self, platform): + m = Module() + wiring.connect(m, wiring.flipped(self.sink), wiring.flipped(self.source)) + return m + +.. warning:: + + It is important to wrap an interface with the :func:`flipped` function whenever it is being connected from inside the module. If the :pc:`elaborate` function above had made a connection using :pc:`wiring.connect(m, self.sink, self.source)`, it would not work correctly. No diagnostic is emitted in this case. + + +.. _wiring-constant-inputs: + +Constant inputs ++++++++++++++++ + +Sometimes, a component must conform to a particular signature, but some of the input ports required by the signature must have a fixed value at all times. This module addresses this case by allowing both :class:`Signal` and :class:`Const` objects to be used to implement port members: + +.. testcode:: + + class ProducerRequiringReady(wiring.Component): + source: Out(SimpleStreamSignature(8)) + + def __init__(self): + super().__init__() + self.source.ready = Const(1) + + def elaborate(self, platform): ... + + + class ConsumerAlwaysReady(wiring.Component): + sink: In(SimpleStreamSignature(8)) + + def __init__(self): + super().__init__() + self.sink.ready = Const(1) + + def elaborate(self, platform): ... + + + class ConsumerPossiblyUnready(wiring.Component): + sink: In(SimpleStreamSignature(8)) + + def elaborate(self, platform): ... + +.. doctest:: + + >>> SimpleStreamSignature(8).is_compliant(ProducerRequiringReady().source) + True + >>> SimpleStreamSignature(8).flip().is_compliant(ConsumerAlwaysReady().sink) + True + +However, the :func:`connect` function considers a constant input to be connectable only to a constant output with the same value: + +.. doctest:: + + >>> wiring.connect(m, ProducerRequiringReady().source, ConsumerAlwaysReady().sink) + >>> wiring.connect(m, ProducerRequiringReady().source, ConsumerPossiblyUnready().sink) + Traceback (most recent call last): + ... + amaranth.lib.wiring.ConnectionError: Cannot connect to the input member 'arg0.ready' that has a constant value 1 + +This feature reduces the proliferation of similar but subtly incompatible interfaces that are semantically similar, only differing in the presence or absence of optional control or status signals. + + +.. _wiring-adapting-interfaces: + +Adapting interfaces ++++++++++++++++++++ + +Sometimes, a design requires an interface with a particular signature to be used, but the only implementation available is either a component with an incompatible signature or an elaboratable with no signature at all. If this problem cannot be resolved by other means, *interface adaptation* can be used, where the existing signals are placed into a new interface with the appropriate signature. For example: + +.. testcode:: + + class LegacyAXIDataProducer(Elaboratable): + def __init__(self): + self.adata = Signal(8) + self.avalid = Signal() + self.aready = Signal() + + def elaborate(self, platform): ... + + + class ModernDataConsumer(wiring.Component): + sink: In(SimpleStreamSignature(8)) + + + data_producer = LegacyAXIDataProducer() + data_consumer = ModernDataConsumer() + + adapted_data_source = SimpleStreamSignature(8).create() + adapted_data_source.data = data_producer.adata + adapted_data_source.valid = data_producer.avalid + adapted_data_source.ready = data_producer.aready + + m = Module() + wiring.connect(m, adapted_data_source, data_consumer.sink) + +When creating an adapted interface, use the :meth:`create ` method of the signature that is required elsewhere in the design. + +.. _wiring-customizing: + +Customizing signatures and interfaces ++++++++++++++++++++++++++++++++++++++ + +The :mod:`amaranth.lib.wiring` module encourages creation of reusable building blocks. In the examples above, a custom signature, :pc:`SimpleStreamSignature`, was introduced to illustrate the essential concepts necessary to use this module. While sufficient for that goal, it does not demonstrate the full capabilities provided by the module. + +Consider a simple System-on-Chip memory bus with a configurable address width. In an application like that, additional properties and methods could be usefully defined both on the signature (for example, properties to retrieve the parameters of the interface) and on the created interface object (for example, methods to examine the control and status signals). These can be defined as follows: + +.. testcode:: + + from amaranth.lib import enum + + + class TransferType(enum.Enum, shape=1): + Write = 0 + Read = 1 + + + class SimpleBusSignature(wiring.Signature): + def __init__(self, addr_width=32): + self._addr_width = addr_width + super().__init__({ + "en": Out(1), + "rw": Out(TransferType), + "addr": Out(self._addr_width), + "r_data": In(32), + "w_data": Out(32), + }) + + @property + def addr_width(self): + return self._addr_width + + def __eq__(self, other): + return isinstance(other, SimpleBusSignature) and self.addr_width == other.addr_width + + def __repr__(self): + return f"SimpleBusSignature({self.addr_width})" + + def create(self, *, path=None, src_loc_at=0): + return SimpleBusInterface(self, path=path, src_loc_at=1 + src_loc_at) + + + class SimpleBusInterface(wiring.PureInterface): + def is_read_xfer(self): + return self.en & (self.rw == TransferType.Read) + + def is_write_xfer(self): + return self.en & (self.rw == TransferType.Write) + +This example demonstrates several important principles of use: + +* Defining additional properties for a custom signature. The :class:`Signature` objects are mutable in a restricted way, and can be frozen with the :meth:`freeze ` method. In almost all cases, the newly defined properties must be immutable, as shown above. +* Defining a signature-specific :pc:`__eq__` method. While anonymous (created from a dictionary of members) instances of :class:`Signature` compare structurally, instances of :class:`Signature`-derived classes compare by identity unless the equality operator is overridden. In almost all cases, the equality operator should compare the parameters of the signatures rather than their structures. +* Defining a signature-specific :pc:`__repr__` method. Similarly to :pc:`__eq__`, the default implementation for :class:`Signature`-derived classes uses the signature's identity. In almost all cases, the representation conversion operator should return an expression that constructs an equivalent signature. +* Defining a signature-specific :pc:`create` method. The default implementation used in anonymous signatures, :meth:`Signature.create`, returns a new instance of :class:`PureInterface`. Whenever the custom signature has a corresponding custom interface object class, this method should return a new instance of that class. It should not have any required arguments beyond the ones that :meth:`Signature.create` has (required parameters should be provided when creating the signature and not the interface), but may take additional optional arguments, forwarding them to the interface object constructor. + +.. doctest:: + + >>> sig32 = SimpleBusSignature(); sig32 + SimpleBusSignature(32) + >>> sig24 = SimpleBusSignature(24); sig24 + SimpleBusSignature(24) + >>> sig24.addr_width + 24 + >>> sig24 == SimpleBusSignature(24) + True + >>> bus = sig24.create(); bus + + >>> bus.is_read_xfer() + (& (sig bus__en) (== (sig bus__rw) (const 1'd1))) + +The custom properties defined for both the signature and the interface object can be used on the flipped signature and the flipped interface in the usual way: + +.. doctest:: + + >>> sig32.flip().addr_width + 32 + >>> wiring.flipped(bus).is_read_xfer() + (& (sig bus__en) (== (sig bus__rw) (const 1'd1))) + +.. note:: + + Unusually for Python, when the implementation of a property or method is invoked through a flipped object, the :pc:`self` argument receives the flipped object that has the type :class:`FlippedSignature` or :class:`FlippedInterface`. This wrapper object proxies all attribute accesses and method calls to the original signature or interface, the only change being that of the data flow directions. See the documentation for these classes for a more detailed explanation. + +.. warning:: + + While the wrapper object forwards attribute accesses and method calls, it does not currently proxy special methods such as :pc:`__getitem__` or :pc:`__add__` that are rarely, if ever, used with interface objects. This limitation may be lifted in the future. + + +.. _wiring-path: + +Paths ++++++ + +Whenever an operation in this module needs to refer to the interior of an object, it accepts or produces a *path*: a tuple of strings and integers denoting the attribute names and indexes through which an interior value can be extracted. For example, the path :pc:`("buses", 0, "cyc")` into the object :pc:`obj` corresponds to the Python expression :pc:`obj.buses[0].cyc`. + +When they appear in diagnostics, paths are printed as the corresponding Python expression. + + +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 + :no-members: + + .. automethod:: flip + +.. autoclass:: Signature +.. autoclass:: FlippedSignature(unflipped) + :no-members: + + .. automethod:: flip + .. automethod:: __getattr__ + .. automethod:: __setattr__ + .. automethod:: __delattr__ + +.. autoclass:: SignatureMeta + + +Interfaces +========== + +.. autoclass:: PureInterface +.. autoclass:: FlippedInterface(unflipped) + +.. autofunction:: flipped + + +Making connections +================== + +.. autoexception:: ConnectionError + +.. autofunction:: connect + + +Components +========== + +.. autoclass:: Component diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py index d72d7bd..206fa88 100644 --- a/tests/test_lib_wiring.py +++ b/tests/test_lib_wiring.py @@ -853,7 +853,8 @@ class ConnectTestCase(unittest.TestCase): def test_out_to_const_in(self): m = Module() with self.assertRaisesRegex(ConnectionError, - r"^Cannot connect to the input member 'q\.a' that has a constant value 0$"): + r"^Cannot connect input member 'q\.a' that has a constant value 0 to an output " + r"member 'p\.a' that has a varying value$"): connect(m, p=NS(signature=Signature({"a": Out(1)}), a=Signal()), @@ -864,7 +865,7 @@ class ConnectTestCase(unittest.TestCase): m = Module() with self.assertRaisesRegex(ConnectionError, r"^Cannot connect input member 'q\.a' that has a constant value 0 to an output " - r"member 'p\.a' that has a differing constant value 1$"): + r"member 'p\.a' that has a different constant value 1$"): connect(m, p=NS(signature=Signature({"a": Out(1)}), a=Const(1)), @@ -986,7 +987,7 @@ class ComponentTestCase(unittest.TestCase): class C(Component): pass - with self.assertRaisesRegex(NotImplementedError, + with self.assertRaisesRegex(TypeError, r"^Component '.+?\.C' does not have signature member annotations$"): C() @@ -1023,7 +1024,7 @@ class ComponentTestCase(unittest.TestCase): class B(A): a: Out(1) - with self.assertRaisesRegex(SignatureError, + with self.assertRaisesRegex(NameError, r"^Member 'a' is redefined in .*.B$"): B()