lib.wiring: implement amaranth-lang/rfcs#2.
Co-authored-by: Charlotte <charlotte@lottia.net>
This commit is contained in:
parent
6e50559dfa
commit
4ffadff20d
2 changed files with 1617 additions and 0 deletions
783
amaranth/lib/wiring.py
Normal file
783
amaranth/lib/wiring.py
Normal file
|
|
@ -0,0 +1,783 @@
|
|||
from collections.abc import Mapping
|
||||
import enum
|
||||
import types
|
||||
import inspect
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from ..hdl.ast import Shape, ShapeCastable, Const, Signal, Value, ValueCastable
|
||||
from ..hdl.ir import Elaboratable
|
||||
from .._utils import final
|
||||
|
||||
|
||||
__all__ = ["In", "Out", "Signature", "connect", "flipped", "Component"]
|
||||
|
||||
|
||||
class Flow(enum.Enum):
|
||||
Out = 0
|
||||
In = 1
|
||||
|
||||
def flip(self):
|
||||
if self == Out:
|
||||
return In
|
||||
if self == In:
|
||||
return Out
|
||||
assert False # :nocov:
|
||||
|
||||
def __call__(self, description, *, reset=None):
|
||||
return Member(self, description, reset=reset)
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
In = Flow.In
|
||||
Out = Flow.Out
|
||||
|
||||
|
||||
@final
|
||||
class Member:
|
||||
def __init__(self, flow, description, *, reset=None, _dimensions=()):
|
||||
self._flow = flow
|
||||
self._description = description
|
||||
self._reset = reset
|
||||
self._dimensions = _dimensions
|
||||
|
||||
# Check that the description is valid, and populate derived properties.
|
||||
if self.is_port:
|
||||
# Cast the description to a shape for typechecking, but keep the original
|
||||
# shape-castable so that it can be provided
|
||||
try:
|
||||
shape = Shape.cast(self._description)
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Port member description must be a shape-castable object or "
|
||||
f"a signature, not {description!r}") from e
|
||||
# This mirrors the logic that handles Signal(reset=).
|
||||
# TODO: We need a simpler way to check for "is this a valid constant initializer"
|
||||
if issubclass(type(self._description), ShapeCastable):
|
||||
try:
|
||||
self._reset_as_const = Const.cast(self._description.const(self._reset))
|
||||
except Exception as e:
|
||||
raise TypeError(f"Port member reset value {self._reset!r} is not a valid "
|
||||
f"constant initializer for {self._description}") from e
|
||||
else:
|
||||
try:
|
||||
self._reset_as_const = Const.cast(reset or 0)
|
||||
except TypeError:
|
||||
raise TypeError(f"Port member reset value {self._reset!r} is not a valid "
|
||||
f"constant initializer for {shape}")
|
||||
if self.is_signature:
|
||||
if self._reset is not None:
|
||||
raise ValueError(f"A signature member cannot have a reset value")
|
||||
|
||||
def flip(self):
|
||||
return Member(self._flow.flip(), self._description, reset=self._reset,
|
||||
_dimensions=self._dimensions)
|
||||
|
||||
def array(self, *dimensions):
|
||||
for dimension in dimensions:
|
||||
if not (isinstance(dimension, int) and dimension >= 0):
|
||||
raise TypeError(f"Member array dimensions must be non-negative integers, "
|
||||
f"not {dimension!r}")
|
||||
return Member(self._flow, self._description, reset=self._reset,
|
||||
_dimensions=(*dimensions, *self._dimensions))
|
||||
|
||||
@property
|
||||
def flow(self):
|
||||
return self._flow
|
||||
|
||||
@property
|
||||
def is_port(self):
|
||||
return not isinstance(self._description, Signature)
|
||||
|
||||
@property
|
||||
def is_signature(self):
|
||||
return isinstance(self._description, Signature)
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
if self.is_signature:
|
||||
raise AttributeError(f"A signature member does not have a shape")
|
||||
return self._description
|
||||
|
||||
@property
|
||||
def reset(self):
|
||||
if self.is_signature:
|
||||
raise AttributeError(f"A signature member does not have a reset value")
|
||||
return self._reset
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
if self.is_port:
|
||||
raise AttributeError(f"A port member does not have a signature")
|
||||
if self.flow == Out:
|
||||
return self._description
|
||||
if self.flow == In:
|
||||
return self._description.flip()
|
||||
assert False # :nocov:
|
||||
|
||||
@property
|
||||
def dimensions(self):
|
||||
return self._dimensions
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(other) is Member and
|
||||
self._flow == other._flow and
|
||||
self._description == other._description and
|
||||
self._reset == other._reset and
|
||||
self._dimensions == other._dimensions)
|
||||
|
||||
def __repr__(self):
|
||||
reset_repr = dimensions_repr = ""
|
||||
if self._reset:
|
||||
reset_repr = f", reset={self._reset!r}"
|
||||
if self._dimensions:
|
||||
dimensions_repr = f".array({', '.join(map(str, self._dimensions))})"
|
||||
return f"{self._flow!r}({self._description!r}{reset_repr}){dimensions_repr}"
|
||||
|
||||
|
||||
@final
|
||||
class SignatureError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# 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):
|
||||
def __init__(self, members=()):
|
||||
self._dict = dict()
|
||||
self._frozen = False
|
||||
self += members
|
||||
|
||||
def flip(self):
|
||||
return FlippedSignatureMembers(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, (SignatureMembers, FlippedSignatureMembers)) and
|
||||
list(self.flatten()) == list(other.flatten()))
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self._dict
|
||||
|
||||
def _check_name(self, name):
|
||||
if not isinstance(name, str):
|
||||
raise TypeError(f"Member name must be a string, not {name!r}")
|
||||
if not re.match(r"^[A-Za-z][0-9A-Za-z_]*$", name):
|
||||
raise NameError(f"Member name '{name}' must be a valid, public Python attribute name")
|
||||
if name == "signature":
|
||||
raise NameError(f"Member name cannot be '{name}'")
|
||||
|
||||
def __getitem__(self, name):
|
||||
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):
|
||||
self._check_name(name)
|
||||
if name in self._dict:
|
||||
raise SignatureError(f"Member '{name}' already exists in the signature and cannot "
|
||||
f"be replaced")
|
||||
if type(member) is not Member:
|
||||
raise TypeError(f"Assigned value {member!r} must be a member; "
|
||||
f"did you mean In({member!r}) or Out({member!r})?")
|
||||
if self._frozen:
|
||||
raise SignatureError("Cannot add members to a frozen signature")
|
||||
self._dict[name] = member
|
||||
|
||||
def __delitem__(self, name):
|
||||
raise SignatureError("Members cannot be removed from a signature")
|
||||
|
||||
def __iter__(self):
|
||||
return iter(sorted(self._dict))
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dict)
|
||||
|
||||
def __iadd__(self, members):
|
||||
for name, member in dict(members).items():
|
||||
self[name] = member
|
||||
return self
|
||||
|
||||
@property
|
||||
def frozen(self):
|
||||
return self._frozen
|
||||
|
||||
def freeze(self):
|
||||
self._frozen = True
|
||||
for member in self.values():
|
||||
if member.is_signature:
|
||||
member.signature.freeze()
|
||||
|
||||
def flatten(self, *, path=()):
|
||||
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=()):
|
||||
attrs = {}
|
||||
for name, member in self.items():
|
||||
def create_value(path):
|
||||
if member.is_port:
|
||||
return Signal(member.shape, reset=member.reset,
|
||||
name="__".join(str(item) for item in path))
|
||||
if member.is_signature:
|
||||
return member.signature.create(path=path)
|
||||
assert False # :nocov:
|
||||
def create_dimensions(dimensions, *, path):
|
||||
if not dimensions:
|
||||
return create_value(path)
|
||||
dimension, *rest_of_dimensions = dimensions
|
||||
return [create_dimensions(rest_of_dimensions, path=(*path, index))
|
||||
for index in range(dimension)]
|
||||
attrs[name] = create_dimensions(member.dimensions, path=(*path, name))
|
||||
return attrs
|
||||
|
||||
def __repr__(self):
|
||||
frozen_repr = ".freeze()" if self._frozen else ""
|
||||
return f"SignatureMembers({self._dict}){frozen_repr}"
|
||||
|
||||
|
||||
@final
|
||||
class FlippedSignatureMembers(Mapping):
|
||||
def __init__(self, unflipped):
|
||||
self.__unflipped = unflipped
|
||||
|
||||
def flip(self):
|
||||
return self.__unflipped
|
||||
|
||||
# See the note below.
|
||||
__eq__ = SignatureMembers.__eq__
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self.__unflipped
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.__unflipped.__getitem__(name).flip()
|
||||
|
||||
def __setitem__(self, name, member):
|
||||
self.__unflipped.__setitem__(name, member.flip())
|
||||
|
||||
def __delitem__(self, name):
|
||||
self.__unflipped.__delitem__(name)
|
||||
|
||||
def __iter__(self):
|
||||
return self.__unflipped.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return self.__unflipped.__len__()
|
||||
|
||||
def __iadd__(self, members):
|
||||
self.__unflipped.__iadd__({name: member.flip() for name, member in members.items()})
|
||||
return self
|
||||
|
||||
@property
|
||||
def frozen(self):
|
||||
return self.__unflipped.frozen
|
||||
|
||||
def freeze(self):
|
||||
self.__unflipped.freeze()
|
||||
|
||||
# These methods do not access instance variables and so their implementation can be shared
|
||||
# between the normal and the flipped member collections.
|
||||
flatten = SignatureMembers.flatten
|
||||
create = SignatureMembers.create
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__unflipped!r}.flip()"
|
||||
|
||||
|
||||
def _format_path(path):
|
||||
first, *rest = path
|
||||
if isinstance(first, int):
|
||||
# only happens in connect()
|
||||
chunks = [f"arg{first}"]
|
||||
else:
|
||||
chunks = [first]
|
||||
for item in rest:
|
||||
if isinstance(item, int):
|
||||
chunks.append(f"[{item}]")
|
||||
else:
|
||||
chunks.append(f".{item}")
|
||||
return f"'{''.join(chunks)}'"
|
||||
|
||||
|
||||
def _traverse_path(path, obj):
|
||||
first, *rest = path
|
||||
obj = obj[first]
|
||||
for item in rest:
|
||||
if isinstance(item, int):
|
||||
obj = obj[item]
|
||||
else:
|
||||
obj = getattr(obj, item)
|
||||
return obj
|
||||
|
||||
|
||||
def _format_shape(shape):
|
||||
if type(shape) is Shape:
|
||||
return f"{shape}"
|
||||
if isinstance(shape, int):
|
||||
return f"{Shape.cast(shape)}"
|
||||
return f"{Shape.cast(shape)} ({shape!r})"
|
||||
|
||||
|
||||
class SignatureMeta(type):
|
||||
def __subclasscheck__(cls, subclass):
|
||||
# `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:
|
||||
return True
|
||||
return super().__subclasscheck__(subclass)
|
||||
|
||||
def __instancecheck__(cls, instance):
|
||||
# `FlippedSignature` is an instance of a `Signature` or its subclass if the unflipped
|
||||
# object is.
|
||||
if type(instance) is FlippedSignature:
|
||||
return super().__instancecheck__(instance.flip())
|
||||
return super().__instancecheck__(instance)
|
||||
|
||||
|
||||
class Signature(metaclass=SignatureMeta):
|
||||
def __init__(self, members):
|
||||
self.__members = SignatureMembers(members)
|
||||
|
||||
def flip(self):
|
||||
return FlippedSignature(self)
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
return self.__members
|
||||
|
||||
def __eq__(self, other):
|
||||
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.
|
||||
return self.members == other.members
|
||||
else:
|
||||
# Otherwise (if `self` refers to a derived class) compare by identity. This will
|
||||
# usually be overridden in a derived class.
|
||||
return self is other
|
||||
|
||||
@property
|
||||
def frozen(self):
|
||||
return self.members.frozen
|
||||
|
||||
def freeze(self):
|
||||
self.members.freeze()
|
||||
return self
|
||||
|
||||
def is_compliant(self, obj, *, reasons=None, path=("obj",)):
|
||||
def check_attr_value(member, attr_value, *, path):
|
||||
if member.is_port:
|
||||
try:
|
||||
attr_value_cast = Value.cast(attr_value)
|
||||
except:
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is not a value-castable object, "
|
||||
f"but {attr_value!r}")
|
||||
return False
|
||||
if not isinstance(attr_value_cast, (Signal, Const)):
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is neither a signal nor a constant, "
|
||||
f"but {attr_value_cast!r}")
|
||||
return False
|
||||
attr_shape = attr_value_cast.shape()
|
||||
if Shape.cast(attr_shape) != Shape.cast(member.shape):
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is expected to have "
|
||||
f"the shape {_format_shape(member.shape)}, but it has "
|
||||
f"the shape {_format_shape(attr_shape)}")
|
||||
return False
|
||||
if isinstance(attr_value_cast, Signal):
|
||||
if attr_value_cast.reset != member._reset_as_const.value:
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is expected to have "
|
||||
f"the reset value {member.reset!r}, but it has "
|
||||
f"the reset value {attr_value_cast.reset!r}")
|
||||
return False
|
||||
if attr_value_cast.reset_less:
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is expected to not be reset-less")
|
||||
return False
|
||||
return True
|
||||
if member.is_signature:
|
||||
return member.signature.is_compliant(attr_value, reasons=reasons, path=path)
|
||||
assert False # :nocov:
|
||||
|
||||
def check_dimensions(member, attr_value, dimensions, *, path):
|
||||
if not dimensions:
|
||||
return check_attr_value(member, attr_value, path=path)
|
||||
|
||||
dimension, *rest_of_dimensions = dimensions
|
||||
if not isinstance(attr_value, (tuple, list)):
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is expected to be a tuple or a list, "
|
||||
f"but it is a {attr_value!r}")
|
||||
return False
|
||||
if len(attr_value) != dimension:
|
||||
if reasons is not None:
|
||||
reasons.append(f"{_format_path(path)} is expected to have dimension "
|
||||
f"{dimension}, but its length is {len(attr_value)}")
|
||||
return False
|
||||
|
||||
result = True
|
||||
for index in range(dimension):
|
||||
if not check_dimensions(member, attr_value[index], rest_of_dimensions,
|
||||
path=(*path, index)):
|
||||
result = False
|
||||
if reasons is None:
|
||||
break # short cicruit if detailed error message isn't required
|
||||
return result
|
||||
|
||||
result = True
|
||||
for attr_name, member in self.members.items():
|
||||
try:
|
||||
attr_value = getattr(obj, attr_name)
|
||||
except AttributeError:
|
||||
if reasons is None:
|
||||
return False
|
||||
else:
|
||||
reasons.append(f"{_format_path(path)} does not have an attribute "
|
||||
f"{attr_name!r}")
|
||||
result = False
|
||||
continue
|
||||
if not check_dimensions(member, attr_value, member.dimensions, path=(*path, attr_name)):
|
||||
if reasons is None:
|
||||
return False
|
||||
else:
|
||||
# `reasons` was mutated by check_dimensions()
|
||||
result = False
|
||||
continue
|
||||
return result
|
||||
|
||||
def create(self, *, path=()):
|
||||
return Interface(self, path=path)
|
||||
|
||||
def __repr__(self):
|
||||
if type(self) is Signature:
|
||||
return f"Signature({dict(self.members.items())})"
|
||||
return super().__repr__()
|
||||
|
||||
|
||||
# To simplify implementation and reduce API surface area `FlippedSignature` is made final. This
|
||||
# restriction could be lifted if there is a compelling use case.
|
||||
@final
|
||||
class FlippedSignature:
|
||||
def __init__(self, signature):
|
||||
object.__setattr__(self, "_FlippedSignature__unflipped", signature)
|
||||
|
||||
def flip(self):
|
||||
return self.__unflipped
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
return FlippedSignatureMembers(self.__unflipped.members)
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is FlippedSignature:
|
||||
# Trivial case.
|
||||
return self.flip() == other.flip()
|
||||
else:
|
||||
# Delegate comparisons back to Signature (or its descendant) by flipping the arguments;
|
||||
# equality must be reflexive but the implementation of __eq__ need not be, and we can
|
||||
# take advantage of it here.
|
||||
return other == self
|
||||
|
||||
# These methods do not access instance variables and so their implementation can be shared
|
||||
# between the normal and the flipped member collections.
|
||||
frozen = Signature.frozen
|
||||
freeze = Signature.freeze
|
||||
is_compliant = Signature.is_compliant
|
||||
create = Signature.create
|
||||
|
||||
# FIXME: document this logic
|
||||
def __getattr__(self, name):
|
||||
value = getattr(self.__unflipped, name)
|
||||
if inspect.ismethod(value):
|
||||
return types.MethodType(value.__func__, self)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
return setattr(self.__unflipped, name, value)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__unflipped!r}.flip()"
|
||||
|
||||
|
||||
class Interface:
|
||||
def __init__(self, signature, *, path):
|
||||
self.__dict__.update({
|
||||
"signature": signature,
|
||||
**signature.members.create(path=path)
|
||||
})
|
||||
|
||||
|
||||
# To reduce API surface area `FlippedInterface` is made final. This restriction could be lifted
|
||||
# if there is a compelling use case.
|
||||
@final
|
||||
class FlippedInterface:
|
||||
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}")
|
||||
object.__setattr__(self, "_FlippedInterface__unflipped", interface)
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
return self.__unflipped.signature.flip()
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) is type(other) and self.__unflipped == other.__unflipped
|
||||
|
||||
# FIXME: document this logic
|
||||
def __getattr__(self, name):
|
||||
value = getattr(self.__unflipped, name)
|
||||
if inspect.ismethod(value):
|
||||
return types.MethodType(value.__func__, self)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
return setattr(self.__unflipped, name, value)
|
||||
|
||||
def __repr__(self):
|
||||
return f"flipped({self.__unflipped!r})"
|
||||
|
||||
|
||||
def flipped(interface):
|
||||
return FlippedInterface(interface)
|
||||
|
||||
|
||||
@final
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def connect(m, *args, **kwargs):
|
||||
objects = {
|
||||
**{index: arg for index, arg in enumerate(args)},
|
||||
**{keyword: arg for keyword, arg in kwargs.items()}
|
||||
}
|
||||
|
||||
# Extract signatures from arguments.
|
||||
signatures = {}
|
||||
for handle, obj in objects.items():
|
||||
if not hasattr(obj, "signature"):
|
||||
raise AttributeError(f"Argument {handle!r} must have a 'signature' attribute")
|
||||
if not isinstance(obj.signature, Signature):
|
||||
raise TypeError(f"Signature of argument {handle!r} must be a signature, "
|
||||
f"not {obj.signature!r}")
|
||||
if not obj.signature.is_compliant(obj):
|
||||
reasons = []
|
||||
obj.signature.is_compliant(obj, reasons=reasons, path=(handle,))
|
||||
reasons_as_string = "".join("\n- " + reason for reason in reasons)
|
||||
raise ConnectionError(f"Argument {handle!r} does not match its signature:" +
|
||||
reasons_as_string)
|
||||
signatures[handle] = obj.signature.freeze()
|
||||
|
||||
# Collate signatures and build connections.
|
||||
flattens = {handle: signature.members.flatten()
|
||||
for handle, signature in signatures.items()}
|
||||
connections = []
|
||||
# Each iteration of the outer loop is intended to connect several (usually a pair) members
|
||||
# to each other, e.g. an out member `[0].a` to an in member `[1].a`. However, because we
|
||||
# do not just check signatures for equality (in order to improve diagnostics), it is possible
|
||||
# that we will find that in `[0]`, the first member is `a`, and in `[1]`, the first member
|
||||
# is completely unrelated `[b]`. Since the assumption that all signatures are equal, or even
|
||||
# of equal length, cannot be made, it is necessary to simultaneously iterate (like with `zip`)
|
||||
# the signature of every object being connected, making sure each set of next members is
|
||||
# is_compliant with each other.
|
||||
while True:
|
||||
# Classify the members by kind and flow: signature, In, Out. Flow of signature members is
|
||||
# implied in the flow of each port member, so the signature members are only classified
|
||||
# here to ensure they are not connected to port members.
|
||||
is_first = True
|
||||
sig_kind, out_kind, in_kind = [], [], []
|
||||
for handle, flattened_members in flattens.items():
|
||||
path_for_handle, member = next(flattened_members, (None, None))
|
||||
# First, ensure that the paths are equal (i.e. that the hierarchy matches for all of
|
||||
# the objects up to this point).
|
||||
if is_first:
|
||||
is_first = False
|
||||
first_path = path_for_handle
|
||||
else:
|
||||
first_handle = next(iter(flattens))
|
||||
if first_path != path_for_handle:
|
||||
# The paths are inequal. It is ambiguous how exactly the diagnostic should be
|
||||
# displayed, and the choices of which other member to use below is arbitrary.
|
||||
# Signature members are iterated in ascending lexicographical order, so the path
|
||||
# that sorts greater corresponds to the handle that's missing a member.
|
||||
if (path_for_handle is None or
|
||||
(first_path is not None and path_for_handle > first_path)):
|
||||
first_path_as_string = _format_path(first_path)
|
||||
raise ConnectionError(f"Member {first_path_as_string} is present in "
|
||||
f"{first_handle!r}, but not in {handle!r}")
|
||||
if (first_path is None or
|
||||
(path_for_handle is not None and path_for_handle < first_path)):
|
||||
path_for_handle_as_string = _format_path(path_for_handle)
|
||||
raise ConnectionError(f"Member {path_for_handle_as_string} is present in "
|
||||
f"{handle!r}, but not in {first_handle!r}")
|
||||
assert False # :nocov:
|
||||
# If there is no actual member, the signature has been fully iterated through.
|
||||
# Other signatures may still have extraneous members, so continue iterating until
|
||||
# a diagnostic is returned.
|
||||
if member is None:
|
||||
continue
|
||||
# At this point we know the paths are equal, but the members can still have
|
||||
# inis_compliant flow, kind (signature or port), signature, or shape. Collect all of
|
||||
# these for later evaluation.
|
||||
if member.is_port:
|
||||
if member.flow == Out:
|
||||
out_kind.append(((handle, *path_for_handle), member))
|
||||
if member.flow == In:
|
||||
in_kind.append(((handle, *path_for_handle), member))
|
||||
if member.is_signature:
|
||||
sig_kind.append(((handle, *path_for_handle), member))
|
||||
# If there's no path and an error wasn't raised above, we're done!
|
||||
if first_path is None:
|
||||
break
|
||||
# At this point, valid possibilities are:
|
||||
# - All of the members are signature members. In this case, we move on to their contents,
|
||||
# and ignore the signatures themselves.
|
||||
# - There are no signature members, and there is exactly one Out flow member. In this case,
|
||||
# this member is connected to the remaining In members, of which there may be any amount.
|
||||
# All other cases must be rejected with a diagnostic.
|
||||
if sig_kind and (out_kind or in_kind):
|
||||
sig_member_paths_as_string = \
|
||||
", ".join(_format_path(h) for h, m in sig_kind)
|
||||
port_member_paths_as_string = \
|
||||
", ".join(_format_path(h) for h, m in out_kind + in_kind)
|
||||
raise ConnectionError(
|
||||
f"Cannot connect signature member(s) {sig_member_paths_as_string} with "
|
||||
f"port member(s) {port_member_paths_as_string}")
|
||||
if sig_kind:
|
||||
# There are no port members at this point; we're done with this path.
|
||||
continue
|
||||
# There are only port members after this point.
|
||||
is_first = True
|
||||
for (path, member) in in_kind + out_kind:
|
||||
member_shape = member.shape
|
||||
if is_first:
|
||||
is_first = False
|
||||
first_path = path
|
||||
first_member_shape = member.shape
|
||||
first_member_reset = member.reset
|
||||
first_member_reset_as_const = member._reset_as_const
|
||||
continue
|
||||
if Shape.cast(first_member_shape).width != Shape.cast(member_shape).width:
|
||||
raise ConnectionError(
|
||||
f"Cannot connect the member {_format_path(first_path)} with shape "
|
||||
f"{_format_shape(first_member_shape)} to the member {_format_path(path)} with "
|
||||
f"shape {_format_shape(member_shape)} because the shape widths "
|
||||
f"({Shape.cast(first_member_shape).width} and "
|
||||
f"{Shape.cast(member_shape).width}) do not match")
|
||||
if first_member_reset_as_const.value != member._reset_as_const.value:
|
||||
raise ConnectionError(
|
||||
f"Cannot connect together the member {_format_path(first_path)} with reset "
|
||||
f"value {first_member_reset!r} and the member {_format_path(path)} with reset "
|
||||
f"value {member.reset} because the reset values do not match")
|
||||
# If there are no Out members, there is nothing to connect. The In members, while not
|
||||
# explicitly connected, will stay at the same value since we ensured their reset values
|
||||
# are all identical.
|
||||
if len(out_kind) == 0:
|
||||
continue
|
||||
# Check that there is only one Out member. In the future we could extend connection to
|
||||
# handle wired-OR and wired-AND, and this check may go away.
|
||||
if len(out_kind) != 1:
|
||||
out_member_paths_as_string = \
|
||||
", ".join(_format_path(h) for h, m in out_kind)
|
||||
raise ConnectionError(
|
||||
f"Cannot connect several output members {out_member_paths_as_string} together")
|
||||
# There is exactly one Out member after this point, and any amount of In members.
|
||||
# Traversing the paths to all of them should always succeed, since the signature check
|
||||
# at the beginning of `connect()` passed, and so should casting the result to a Value.
|
||||
(out_path, out_member), = out_kind
|
||||
for (in_path, in_member) in in_kind:
|
||||
def connect_value(*, out_path, in_path):
|
||||
in_value = Value.cast(_traverse_path(in_path, objects))
|
||||
out_value = Value.cast(_traverse_path(out_path, objects))
|
||||
assert type(in_value) in (Const, Signal)
|
||||
# If the input is a constant, only a constant may be connected to it. Ensure that
|
||||
# this is the case.
|
||||
if type(in_value) is Const:
|
||||
# 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}")
|
||||
# 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"{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
|
||||
# been made.
|
||||
return
|
||||
# A connection that is made at this point is guaranteed to be valid.
|
||||
connections.append(in_value.eq(out_value))
|
||||
def connect_dimensions(dimensions, *, out_path, in_path):
|
||||
if not dimensions:
|
||||
return connect_value(out_path=out_path, in_path=in_path)
|
||||
dimension, *rest_of_dimensions = dimensions
|
||||
for index in range(dimension):
|
||||
connect_dimensions(rest_of_dimensions,
|
||||
out_path=(*out_path, index), in_path=(*in_path, index))
|
||||
assert out_member.dimensions == in_member.dimensions
|
||||
connect_dimensions(out_member.dimensions, out_path=out_path, in_path=in_path)
|
||||
# Now that we know all of the connections are legal, add them to the module. This is done
|
||||
# instead of returning them because adding them to a non-comb domain would subtly violate
|
||||
# assumptions that `connect()` is intended to provide.
|
||||
m.d.comb += connections
|
||||
|
||||
|
||||
class Component(Elaboratable):
|
||||
def __init__(self):
|
||||
for name in self.signature.members:
|
||||
if hasattr(self, name):
|
||||
raise NameError(f"Cannot initialize attribute for signature member {name!r} "
|
||||
f"because an attribute with the same name already exists")
|
||||
self.__dict__.update(self.signature.members.create())
|
||||
|
||||
# TODO(py3.9): This should be a class method, but descriptors don't stack this way
|
||||
# in Python 3.8 and below.
|
||||
# @classmethod
|
||||
@property
|
||||
def signature(self):
|
||||
cls = type(self)
|
||||
signature = Signature({})
|
||||
for base in cls.mro()[:cls.mro().index(Component)]:
|
||||
for name, annot in getattr(base, "__annotations__", {}).items():
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if (annot in (Value, Signal, Const) or
|
||||
(isinstance(annot, type) and issubclass(annot, ValueCastable)) or
|
||||
isinstance(annot, Signature)):
|
||||
if isinstance(annot, type):
|
||||
annot_repr = annot.__name__
|
||||
else:
|
||||
annot_repr = repr(annot)
|
||||
# To suppress this warning in the rare cases where it is necessary (and naming
|
||||
# the field with a leading underscore is infeasible), override the property.
|
||||
warnings.warn(
|
||||
message=f"Component '{cls.__module__}.{cls.__qualname__}' has "
|
||||
f"an annotation '{name}: {annot_repr}', which is not "
|
||||
f"a signature member; did you mean '{name}: In({annot_repr})' "
|
||||
f"or '{name}: Out({annot_repr})'?",
|
||||
category=SyntaxWarning,
|
||||
stacklevel=2)
|
||||
elif type(annot) is Member:
|
||||
signature.members[name] = annot
|
||||
if not signature.members:
|
||||
raise NotImplementedError(
|
||||
f"Component '{cls.__module__}.{cls.__qualname__}' does not have signature member "
|
||||
f"annotations")
|
||||
return signature
|
||||
Loading…
Add table
Add a link
Reference in a new issue