lib.wiring: implement amaranth-lang/rfcs#2.

Co-authored-by: Charlotte <charlotte@lottia.net>
This commit is contained in:
Catherine 2023-06-28 14:56:53 +00:00
parent 6e50559dfa
commit 4ffadff20d
2 changed files with 1617 additions and 0 deletions

783
amaranth/lib/wiring.py Normal file
View 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

834
tests/test_lib_wiring.py Normal file
View file

@ -0,0 +1,834 @@
import unittest
from types import SimpleNamespace as NS
from amaranth import *
from amaranth.hdl.ast import ValueCastable
from amaranth.lib import data, enum
from amaranth.lib.wiring import Flow, In, Out, Member
from amaranth.lib.wiring import SignatureError, SignatureMembers, FlippedSignatureMembers
from amaranth.lib.wiring import Signature, FlippedSignature, Interface, FlippedInterface
from amaranth.lib.wiring import Component
from amaranth.lib.wiring import ConnectionError, connect, flipped
class FlowTestCase(unittest.TestCase):
def test_flow_call(self):
self.assertEqual(In(unsigned(1)), Member(Flow.In, unsigned(1)))
self.assertEqual(Out(5), Member(Flow.Out, 5))
def test_flow_repr(self):
self.assertEqual(repr(Flow.In), "In")
self.assertEqual(repr(Flow.Out), "Out")
def test_flow_str(self):
self.assertEqual(str(Flow.In), "In")
self.assertEqual(str(Flow.Out), "Out")
class MemberTestCase(unittest.TestCase):
def test_port_member(self):
member = Member(In, unsigned(1))
self.assertEqual(member.flow, In)
self.assertEqual(member.is_port, True)
self.assertEqual(member.shape, unsigned(1))
self.assertEqual(member.reset, None)
self.assertEqual(member.is_signature, False)
with self.assertRaisesRegex(AttributeError,
r"^A port member does not have a signature$"):
member.signature
self.assertEqual(member.dimensions, ())
self.assertEqual(repr(member), "In(unsigned(1))")
def test_port_member_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^Port member description must be a shape-castable object or a signature, "
r"not 'whatever'$"):
Member(In, "whatever")
def test_port_member_reset(self):
member = Member(Out, unsigned(1), reset=1)
self.assertEqual(member.flow, Out)
self.assertEqual(member.shape, unsigned(1))
self.assertEqual(member.reset, 1)
self.assertEqual(repr(member._reset_as_const), repr(Const(1, 1)))
self.assertEqual(repr(member), "Out(unsigned(1), reset=1)")
def test_port_member_reset_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^Port member reset value 'no' is not a valid constant initializer "
r"for unsigned\(1\)$"):
Member(In, 1, reset="no")
def test_port_member_reset_shape_castable(self):
layout = data.StructLayout({"a": 32})
member = Member(In, layout, reset={"a": 1})
self.assertEqual(member.flow, In)
self.assertEqual(member.shape, layout)
self.assertEqual(member.reset, {"a": 1})
self.assertEqual(repr(member), "In(StructLayout({'a': 32}), reset={'a': 1})")
def test_port_member_reset_shape_castable_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^Port member reset value 'no' is not a valid constant initializer "
r"for StructLayout\({'a': 32}\)$"):
Member(In, data.StructLayout({"a": 32}), reset="no")
def test_signature_member_out(self):
sig = Signature({"data": Out(unsigned(32))})
member = Member(Out, sig)
self.assertEqual(member.flow, Out)
self.assertEqual(member.is_port, False)
with self.assertRaisesRegex(AttributeError,
r"^A signature member does not have a shape$"):
member.shape
with self.assertRaisesRegex(AttributeError,
r"^A signature member does not have a reset value$"):
member.reset
self.assertEqual(member.is_signature, True)
self.assertEqual(member.signature, sig)
self.assertEqual(member.dimensions, ())
self.assertEqual(repr(member), "Out(Signature({'data': Out(unsigned(32))}))")
def test_signature_member_in(self):
sig = Signature({"data": In(unsigned(32))})
member = Member(In, sig)
self.assertEqual(member.flow, In)
self.assertEqual(member.is_port, False)
with self.assertRaisesRegex(AttributeError,
r"^A signature member does not have a shape$"):
member.shape
with self.assertRaisesRegex(AttributeError,
r"^A signature member does not have a reset value$"):
member.reset
self.assertEqual(member.is_signature, True)
self.assertEqual(member.signature, sig.flip())
self.assertEqual(member.dimensions, ())
self.assertEqual(repr(member), "In(Signature({'data': In(unsigned(32))}))")
def test_signature_member_wrong(self):
with self.assertRaisesRegex(ValueError,
r"^A signature member cannot have a reset value$"):
Member(In, Signature({}), reset=1)
def test_array(self):
array_2 = Member(In, unsigned(1)).array(2)
self.assertEqual(array_2.dimensions, (2,))
self.assertEqual(repr(array_2), "In(unsigned(1)).array(2)")
array_2_3 = Member(In, unsigned(1)).array(2, 3)
self.assertEqual(array_2_3.dimensions, (2, 3))
self.assertEqual(repr(array_2_3), "In(unsigned(1)).array(2, 3)")
array_2_3_chained = Member(In, unsigned(1)).array(3).array(2)
self.assertEqual(array_2_3_chained.dimensions, (2, 3))
self.assertEqual(repr(array_2_3_chained), "In(unsigned(1)).array(2, 3)")
def test_array_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^Member array dimensions must be non-negative integers, not -1$"):
Member(In, unsigned(1)).array(-1)
with self.assertRaisesRegex(TypeError,
r"^Member array dimensions must be non-negative integers, not 'what'$"):
Member(In, unsigned(1)).array("what")
def test_flip(self):
self.assertEqual(In(1).flip(), Out(1))
self.assertEqual(Out(1).flip(), In(1))
def test_equality(self):
self.assertEqual(In(1), In(1))
self.assertNotEqual(In(1), Out(1))
self.assertNotEqual(In(1), In(1, reset=1))
self.assertNotEqual(In(1), In(1, reset=0))
self.assertEqual(In(1), In(1).array())
self.assertNotEqual(In(1), In(1).array(1))
sig = Signature({})
self.assertEqual(In(sig), In(sig))
self.assertNotEqual(In(1), In(Signature({})))
class SignatureMembersTestCase(unittest.TestCase):
def test_contains(self):
self.assertNotIn("a", SignatureMembers())
self.assertIn("a", SignatureMembers({"a": In(1)}))
def test_getitem(self):
members = SignatureMembers({"a": In(1)})
self.assertEqual(members["a"], In(1))
def test_getitem_missing(self):
members = SignatureMembers({"a": In(1)})
with self.assertRaisesRegex(SignatureError,
r"^Member 'b' is not a part of the signature$"):
members["b"]
def test_getitem_wrong(self):
members = SignatureMembers({"a": In(1)})
with self.assertRaisesRegex(TypeError,
r"^Member name must be a string, not 1$"):
members[1]
with self.assertRaisesRegex(NameError,
r"^Member name '_a' must be a valid, public Python attribute name$"):
members["_a"]
with self.assertRaisesRegex(NameError,
r"^Member name cannot be 'signature'$"):
members["signature"]
def test_setitem(self):
members = SignatureMembers()
members["a"] = In(1)
self.assertEqual(members["a"], In(1))
def test_setitem_existing(self):
members = SignatureMembers({"a": In(1)})
with self.assertRaisesRegex(SignatureError,
r"^Member 'a' already exists in the signature and cannot be replaced$"):
members["a"] = Out(2)
def test_setitem_wrong(self):
members = SignatureMembers()
with self.assertRaisesRegex(TypeError,
r"^Member name must be a string, not 1$"):
members[1] = Out(1)
with self.assertRaisesRegex(TypeError,
r"^Assigned value 1 must be a member; did you mean In\(1\) or Out\(1\)\?$"):
members["a"] = 1
with self.assertRaisesRegex(NameError,
r"^Member name '_a' must be a valid, public Python attribute name$"):
members["_a"] = Out(1)
with self.assertRaisesRegex(NameError,
r"^Member name cannot be 'signature'$"):
members["signature"] = Out(1)
def test_delitem(self):
members = SignatureMembers()
with self.assertRaisesRegex(SignatureError,
r"^Members cannot be removed from a signature$"):
del members["a"]
def test_iter_len(self):
members = SignatureMembers()
self.assertEqual(list(iter(members)), [])
self.assertEqual(len(members), 0)
members["a"] = In(1)
self.assertEqual(list(iter(members)), ["a"])
self.assertEqual(len(members), 1)
def test_iter_sorted(self):
self.assertEqual(list(iter(SignatureMembers({"a": In(1), "b": Out(1)}))),
["a", "b"])
self.assertEqual(list(iter(SignatureMembers({"b": In(1), "a": Out(1)}))),
["a", "b"])
def test_iadd(self):
members = SignatureMembers()
members += {"a": In(1)}
members += [("b", Out(1))]
self.assertEqual(members, SignatureMembers({"a": In(1), "b": Out(1)}))
def test_freeze(self):
members = SignatureMembers({"a": In(1)})
self.assertEqual(members.frozen, False)
members.freeze()
self.assertEqual(members.frozen, True)
with self.assertRaisesRegex(SignatureError,
r"^Cannot add members to a frozen signature$"):
members += {"b": Out(1)}
def test_freeze_rec(self):
sig = Signature({})
members = SignatureMembers({
"a": In(1),
"s": Out(sig)
})
self.assertEqual(members.frozen, False)
self.assertEqual(sig.frozen, False)
self.assertEqual(sig.members.frozen, False)
members.freeze()
self.assertEqual(members.frozen, True)
self.assertEqual(sig.frozen, True)
self.assertEqual(sig.members.frozen, True)
def test_flatten(self):
sig = Signature({
"b": Out(1),
"c": In(2)
})
members = SignatureMembers({
"a": In(1),
"s": Out(sig)
})
self.assertEqual(list(members.flatten()), [
(("a",), In(1)),
(("s",), Out(sig)),
(("s", "b"), Out(1)),
(("s", "c"), In(2)),
])
def test_create(self):
sig = Signature({
"b": Out(2)
})
members = SignatureMembers({
"a": In(1),
"s": Out(sig)
})
attrs = members.create()
self.assertEqual(list(attrs.keys()), ["a", "s"])
self.assertIsInstance(attrs["a"], Signal)
self.assertEqual(attrs["a"].shape(), unsigned(1))
self.assertEqual(attrs["a"].name, "a")
self.assertEqual(attrs["s"].b.shape(), unsigned(2))
self.assertEqual(attrs["s"].b.name, "s__b")
def test_create_reset(self):
members = SignatureMembers({
"a": In(1, reset=1),
})
attrs = members.create()
self.assertEqual(attrs["a"].reset, 1)
def test_create_tuple(self):
sig = SignatureMembers({
"a": Out(1).array(2, 3)
})
members = sig.create()
self.assertEqual(len(members["a"]), 2)
self.assertEqual(len(members["a"][0]), 3)
self.assertEqual(len(members["a"][1]), 3)
for x in members["a"]:
for y in x:
self.assertIsInstance(y, Signal)
self.assertEqual(members["a"][1][2].name, "a__1__2")
def test_repr(self):
self.assertEqual(repr(SignatureMembers({})),
"SignatureMembers({})")
self.assertEqual(repr(SignatureMembers({"a": In(1)})),
"SignatureMembers({'a': In(1)})")
members = SignatureMembers({"b": Out(2)})
members.freeze()
self.assertEqual(repr(members),
"SignatureMembers({'b': Out(2)}).freeze()")
class FlippedSignatureMembersTestCase(unittest.TestCase):
def test_basic(self):
members = SignatureMembers({"a": In(1)})
fmembers = members.flip()
self.assertIsInstance(fmembers, FlippedSignatureMembers)
self.assertIn("a", fmembers)
self.assertEqual(fmembers["a"], Out(1))
fmembers["b"] = Out(2)
self.assertEqual(len(fmembers), 2)
self.assertEqual(members["b"], In(2))
self.assertEqual(list(fmembers), ["a", "b"])
fmembers += {"c": In(2)}
self.assertEqual(members["c"], Out(2))
self.assertIs(fmembers.flip(), members)
def test_eq(self):
self.assertEqual(SignatureMembers({"a": In(1)}).flip(),
SignatureMembers({"a": In(1)}).flip())
self.assertEqual(SignatureMembers({"a": In(1)}).flip(),
SignatureMembers({"a": Out(1)}))
def test_delitem(self):
fmembers = SignatureMembers().flip()
with self.assertRaisesRegex(SignatureError,
r"^Members cannot be removed from a signature$"):
del fmembers["a"]
def test_freeze(self):
members = SignatureMembers({"a": In(1)})
fmembers = members.flip()
self.assertEqual(fmembers.frozen, False)
fmembers.freeze()
self.assertEqual(members.frozen, True)
self.assertEqual(fmembers.frozen, True)
def test_repr(self):
fmembers = SignatureMembers({"a": In(1)}).flip()
self.assertEqual(repr(fmembers), "SignatureMembers({'a': In(1)}).flip()")
class SignatureTestCase(unittest.TestCase):
def test_create(self):
sig = Signature({"a": In(1)})
self.assertEqual(sig.members, SignatureMembers({"a": In(1)}))
def test_eq(self):
self.assertEqual(Signature({"a": In(1)}),
Signature({"a": In(1)}))
self.assertNotEqual(Signature({"a": In(1)}),
Signature({"a": Out(1)}))
def test_freeze(self):
sig = Signature({"a": In(1)})
self.assertEqual(sig.frozen, False)
sig.freeze()
self.assertEqual(sig.frozen, True)
with self.assertRaisesRegex(SignatureError,
r"^Cannot add members to a frozen signature$"):
sig.members += {"b": Out(1)}
def assertNotCompliant(self, reason_regex, sig, obj):
self.assertFalse(sig.is_compliant(obj))
reasons = []
self.assertFalse(sig.is_compliant(obj, reasons=reasons))
self.assertEqual(len(reasons), 1)
self.assertRegex(reasons[0], reason_regex)
def test_is_compliant(self):
self.assertNotCompliant(
r"^'obj' does not have an attribute 'a'$",
sig=Signature({"a": In(1)}),
obj=NS())
self.assertNotCompliant(
r"^'obj\.a' is expected to be a tuple or a list, but it is a \(sig \$signal\)$",
sig=Signature({"a": In(1).array(2)}),
obj=NS(a=Signal()))
self.assertNotCompliant(
r"^'obj\.a' is expected to have dimension 2, but its length is 1$",
sig=Signature({"a": In(1).array(2)}),
obj=NS(a=[Signal()]))
self.assertNotCompliant(
r"^'obj\.a\[0\]' is expected to have dimension 2, but its length is 1$",
sig=Signature({"a": In(1).array(1, 2)}),
obj=NS(a=[[Signal()]]))
self.assertNotCompliant(
r"^'obj\.a' is not a value-castable object, but 'foo'$",
sig=Signature({"a": In(1)}),
obj=NS(a="foo"))
self.assertNotCompliant(
r"^'obj\.a' is neither a signal nor a constant, but "
r"\(\+ \(const 1'd1\) \(const 1'd1\)\)$",
sig=Signature({"a": In(1)}),
obj=NS(a=Const(1)+1))
self.assertNotCompliant(
r"^'obj\.a' is expected to have the shape unsigned\(1\), but "
r"it has the shape unsigned\(2\)$",
sig=Signature({"a": In(1)}),
obj=NS(a=Signal(2)))
self.assertNotCompliant(
r"^'obj\.a' is expected to have the shape unsigned\(1\), but "
r"it has the shape signed\(1\)$",
sig=Signature({"a": In(unsigned(1))}),
obj=NS(a=Signal(signed(1))))
self.assertNotCompliant(
r"^'obj\.a' is expected to have the reset value None, but it has the reset value 1$",
sig=Signature({"a": In(1)}),
obj=NS(a=Signal(reset=1)))
self.assertNotCompliant(
r"^'obj\.a' is expected to have the reset value 1, but it has the reset value 0$",
sig=Signature({"a": In(1, reset=1)}),
obj=NS(a=Signal(1)))
self.assertNotCompliant(
r"^'obj\.a' is expected to not be reset-less$",
sig=Signature({"a": In(1)}),
obj=NS(a=Signal(1, reset_less=True)))
self.assertNotCompliant(
r"^'obj\.a' does not have an attribute 'b'$",
sig=Signature({"a": Out(Signature({"b": In(1)}))}),
obj=NS(a=Signal()))
self.assertTrue(
Signature({"a": In(1)}).is_compliant(
NS(a=Signal())))
self.assertTrue(
Signature({"a": In(1)}).is_compliant(
NS(a=Const(1))))
self.assertTrue( # list
Signature({"a": In(1).array(2, 2)}).is_compliant(
NS(a=[[Const(1), Const(1)], [Signal(), Signal()]])))
self.assertTrue( # tuple
Signature({"a": In(1).array(2, 2)}).is_compliant(
NS(a=((Const(1), Const(1)), (Signal(), Signal())))))
self.assertTrue( # mixed list and tuple
Signature({"a": In(1).array(2, 2)}).is_compliant(
NS(a=[[Const(1), Const(1)], (Signal(), Signal())])))
self.assertTrue(
Signature({"a": Out(Signature({"b": In(1)}))}).is_compliant(
NS(a=NS(b=Signal()))))
def test_repr(self):
sig = Signature({"a": In(1)})
self.assertEqual(repr(sig), "Signature({'a': In(1)})")
def test_repr_subclass(self):
class S(Signature):
def __init__(self):
super().__init__({"a": In(1)})
sig = S()
self.assertRegex(repr(sig), r"^<.+\.S object at .+?>$")
def test_subclasscheck(self):
class S(Signature):
pass
self.assertTrue(issubclass(FlippedSignature, Signature))
self.assertTrue(issubclass(Signature, Signature))
self.assertTrue(issubclass(FlippedSignature, S))
self.assertTrue(not issubclass(Signature, S))
def test_instancecheck(self):
class S(Signature):
pass
sig = Signature({})
sig2 = S({})
self.assertTrue(isinstance(sig.flip(), Signature))
self.assertTrue(isinstance(sig2.flip(), Signature))
self.assertTrue(not isinstance(sig.flip(), S))
self.assertTrue(isinstance(sig2.flip(), S))
class FlippedSignatureTestCase(unittest.TestCase):
def test_create(self):
sig = Signature({"a": In(1)})
fsig = sig.flip()
self.assertIsInstance(fsig, FlippedSignature)
self.assertIsInstance(fsig.members, FlippedSignatureMembers)
self.assertIs(fsig.flip(), sig)
def test_eq(self):
self.assertEqual(Signature({"a": In(1)}).flip(),
Signature({"a": In(1)}).flip())
self.assertEqual(Signature({"a": In(1)}).flip(),
Signature({"a": Out(1)}))
def test_repr(self):
sig = Signature({"a": In(1)}).flip()
self.assertEqual(repr(sig), "Signature({'a': In(1)}).flip()")
def test_getattr_setattr(self):
class S(Signature):
def __init__(self):
super().__init__({})
self.x = 1
def f(self2):
self.assertIsInstance(self2, FlippedSignature)
return "f()"
sig = S()
fsig = sig.flip()
self.assertEqual(fsig.x, 1)
self.assertEqual(fsig.f(), "f()")
fsig.y = 2
self.assertEqual(sig.y, 2)
class InterfaceTestCase(unittest.TestCase):
pass
class FlippedInterfaceTestCase(unittest.TestCase):
def test_basic(self):
sig = Signature({"a": In(1)})
intf = sig.create()
self.assertTrue(sig.is_compliant(intf))
self.assertIs(intf.signature, sig)
tintf = flipped(intf)
self.assertEqual(tintf.signature, intf.signature.flip())
self.assertEqual(tintf, flipped(intf))
self.assertRegex(repr(tintf),
r"^flipped\(<.+?\.Interface object at .+>\)$")
def test_getattr_setattr(self):
class I(Interface):
signature = Signature({})
def __init__(self):
self.x = 1
def f(self2):
self.assertIsInstance(self2, FlippedInterface)
return "f()"
intf = I()
tintf = flipped(intf)
self.assertEqual(tintf.x, 1)
self.assertEqual(tintf.f(), "f()")
tintf.y = 2
self.assertEqual(intf.y, 2)
def test_flipped_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^flipped\(\) can only flip an interface object, not Signature\({}\)$"):
flipped(Signature({}))
class ConnectTestCase(unittest.TestCase):
def test_arg_handles_and_signature_attr(self):
m = Module()
with self.assertRaisesRegex(AttributeError,
r"^Argument 0 must have a 'signature' attribute$"):
connect(m, object())
with self.assertRaisesRegex(AttributeError,
r"^Argument 'x' must have a 'signature' attribute$"):
connect(m, x=object())
def test_signature_type(self):
m = Module()
with self.assertRaisesRegex(TypeError,
r"^Signature of argument 0 must be a signature, not 1$"):
connect(m, NS(signature=1))
def test_signature_compliant(self):
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Argument 0 does not match its signature:\n"
r"- 'arg0' does not have an attribute 'a'$"):
connect(m, NS(signature=Signature({"a": In(1)})))
def test_signature_freeze(self):
m = Module()
intf = NS(signature=Signature({}))
connect(m, intf)
self.assertTrue(intf.signature.frozen)
def test_member_missing(self):
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Member 'b' is present in 'q', but not in 'p'$"):
connect(m,
p=NS(signature=Signature({"a": In(1)}),
a=Signal()),
q=NS(signature=Signature({"a": In(1), "b": Out(1)}),
a=Signal(), b=Signal()))
with self.assertRaisesRegex(ConnectionError,
r"^Member 'b' is present in 'p', but not in 'q'$"):
connect(m,
p=NS(signature=Signature({"a": In(1), "b": Out(1)}),
a=Signal(), b=Signal()),
q=NS(signature=Signature({"a": In(1)}),
a=Signal()))
def test_signature_to_port(self):
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Cannot connect signature member\(s\) 'p\.a' with port member\(s\) 'q\.a'$"):
connect(m,
p=NS(signature=Signature({"a": Out(Signature({}))}),
a=NS(signature=Signature({}))),
q=NS(signature=Signature({"a": In(1)}),
a=Signal()))
def test_shape_mismatch(self):
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Cannot connect the member 'q\.a' with shape unsigned\(2\) to the member 'p\.a' "
r"with shape unsigned\(1\) because the shape widths \(2 and 1\) do not match$"):
connect(m,
p=NS(signature=Signature({"a": Out(1)}),
a=Signal()),
q=NS(signature=Signature({"a": In(2)}),
a=Signal(2)))
def test_shape_mismatch_enum(self):
class Cycle(enum.Enum, shape=2):
READ = 0
WRITE = 1
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Cannot connect the member 'q\.a' with shape unsigned\(2\) \(<enum 'Cycle'>\) "
r"to the member 'p\.a' with shape unsigned\(1\) because the shape widths "
r"\(2 and 1\) do not match$"):
connect(m,
p=NS(signature=Signature({"a": Out(1)}),
a=Signal()),
q=NS(signature=Signature({"a": In(Cycle)}),
a=Signal(Cycle)))
def test_reset_mismatch(self):
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Cannot connect together the member 'q\.a' with reset value 1 and the member "
r"'p\.a' with reset value 0 because the reset values do not match$"):
connect(m,
p=NS(signature=Signature({"a": Out(1, reset=0)}),
a=Signal()),
q=NS(signature=Signature({"a": In(1, reset=1)}),
a=Signal(reset=1)))
def test_reset_none_match(self):
m = Module()
connect(m,
p=NS(signature=Signature({"a": Out(1, reset=0)}),
a=Signal()),
q=NS(signature=Signature({"a": In(1)}),
a=Signal()))
def test_out_to_out(self):
m = Module()
with self.assertRaisesRegex(ConnectionError,
r"^Cannot connect several output members 'p\.a', 'q\.a' together$"):
connect(m,
p=NS(signature=Signature({"a": Out(1)}),
a=Signal()),
q=NS(signature=Signature({"a": Out(1)}),
a=Signal()))
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$"):
connect(m,
p=NS(signature=Signature({"a": Out(1)}),
a=Signal()),
q=NS(signature=Signature({"a": In(1)}),
a=Const(0)))
def test_const_out_to_const_in_value_mismatch(self):
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$"):
connect(m,
p=NS(signature=Signature({"a": Out(1)}),
a=Const(1)),
q=NS(signature=Signature({"a": In(1)}),
a=Const(0)))
def test_simple_bus(self):
class Cycle(enum.Enum):
IDLE = 0
READ = 1
WRITE = 2
sig = Signature({
"cycle": Out(Cycle),
"addr": Out(16),
"r_data": In(32),
"w_data": Out(32),
})
src = sig.create(path=('src',))
snk = sig.flip().create(path=('snk',))
m = Module()
connect(m, src=src, snk=snk)
self.assertEqual([repr(stmt) for stmt in m._statements], [
'(eq (sig snk__addr) (sig src__addr))',
'(eq (sig snk__cycle) (sig src__cycle))',
'(eq (sig src__r_data) (sig snk__r_data))',
'(eq (sig snk__w_data) (sig src__w_data))'
])
def test_const_in_out(self):
m = Module()
connect(m,
p=NS(signature=Signature({"a": Out(1)}),
a=Const(1)),
q=NS(signature=Signature({"a": In(1)}),
a=Const(1)))
self.assertEqual(m._statements, [])
def test_nested(self):
m = Module()
connect(m,
p=NS(signature=Signature({"a": Out(Signature({"f": Out(1)}))}),
a=NS(f=Signal(name='p__a'))),
q=NS(signature=Signature({"a": In(Signature({"f": Out(1)}))}),
a=NS(f=Signal(name='q__a'))))
self.assertEqual([repr(stmt) for stmt in m._statements], [
'(eq (sig q__a) (sig p__a))'
])
def test_dimension(self):
sig = Signature({"a": Out(1).array(2)})
m = Module()
connect(m, p=sig.create(path=('p',)), q=sig.flip().create(path=('q',)))
self.assertEqual([repr(stmt) for stmt in m._statements], [
'(eq (sig q__a__0) (sig p__a__0))',
'(eq (sig q__a__1) (sig p__a__1))'
])
def test_dimension_multi(self):
sig = Signature({"a": Out(1).array(1).array(1)})
m = Module()
connect(m, p=sig.create(path=('p',)), q=sig.flip().create(path=('q',)))
self.assertEqual([repr(stmt) for stmt in m._statements], [
'(eq (sig q__a__0__0) (sig p__a__0__0))',
])
class ComponentTestCase(unittest.TestCase):
def test_basic(self):
class C(Component):
sig : Out(2)
c = C()
self.assertEqual(c.signature, Signature({"sig": Out(2)}))
self.assertIsInstance(c.sig, Signal)
self.assertEqual(c.sig.shape(), unsigned(2))
def test_non_member_annotations(self):
class C(Component):
sig : Out(2)
foo : int
c = C()
self.assertEqual(c.signature, Signature({"sig": Out(2)}))
def test_private_member_annotations(self):
class C(Component):
sig_pub : Out(2)
_sig_priv : Out(2)
c = C()
self.assertEqual(c.signature, Signature({"sig_pub": Out(2)}))
def test_no_annotations(self):
class C(Component):
pass
with self.assertRaisesRegex(NotImplementedError,
r"^Component '.+?\.C' does not have signature member annotations$"):
C()
def test_would_overwrite_field(self):
class C(Component):
sig : Out(2)
def __init__(self):
self.sig = 1
super().__init__()
with self.assertRaisesRegex(NameError,
r"^Cannot initialize attribute for signature member 'sig' because an attribute "
r"with the same name already exists$"):
C()
def test_missing_in_out_warning(self):
class C1(Component):
prt1 : In(1)
sig2 : Signal
with self.assertWarnsRegex(SyntaxWarning,
r"^Component '.+\.C1' has an annotation 'sig2: Signal', which is not a signature "
r"member; did you mean 'sig2: In\(Signal\)' or 'sig2: Out\(Signal\)'\?$"):
C1().signature
class C2(Component):
prt1 : In(1)
sig2 : Signature({})
with self.assertWarnsRegex(SyntaxWarning,
r"^Component '.+\.C2' has an annotation 'sig2: Signature\({}\)', which is not "
r"a signature member; did you mean 'sig2: In\(Signature\({}\)\)' or "
r"'sig2: Out\(Signature\({}\)\)'\?$"):
C2().signature
class MockValueCastable(ValueCastable):
def shape(self): pass
@ValueCastable.lowermethod
def as_value(self): pass
class C3(Component):
prt1 : In(1)
val2 : MockValueCastable
with self.assertWarnsRegex(SyntaxWarning,
r"^Component '.+\.C3' has an annotation 'val2: MockValueCastable', which is not "
r"a signature member; did you mean 'val2: In\(MockValueCastable\)' or "
r"'val2: Out\(MockValueCastable\)'\?$"):
C3().signature