X-Git-Url: https://git.libre-soc.org/?a=blobdiff_plain;f=nmigen%2Fhdl%2Fast.py;h=0b86e7153da0cac1dd5d62c83cca15caaf7357e2;hb=06c734992fd46b6f11bdfa4425ef102640aecf3f;hp=622c0fd04659886e98531d7835453f74f817cad9;hpb=82903e493a63439ba647861f491961fe48358f17;p=nmigen.git diff --git a/nmigen/hdl/ast.py b/nmigen/hdl/ast.py index 622c0fd..0b86e71 100644 --- a/nmigen/hdl/ast.py +++ b/nmigen/hdl/ast.py @@ -1,52 +1,159 @@ from abc import ABCMeta, abstractmethod -import builtins import traceback +import sys +import warnings +import typing +import functools from collections import OrderedDict from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence from enum import Enum from .. import tracer -from ..tools import * +from .._utils import * +from .._unused import * __all__ = [ + "Shape", "signed", "unsigned", "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl", "Array", "ArrayProxy", - "Sample", "Past", "Stable", "Rose", "Fell", "Signal", "ClockSignal", "ResetSignal", - "UserValue", - "Statement", "Assign", "Assert", "Assume", "Switch", "Delay", "Tick", - "Passive", "ValueKey", "ValueDict", "ValueSet", "SignalKey", "SignalDict", - "SignalSet", + "UserValue", "ValueCastable", + "Sample", "Past", "Stable", "Rose", "Fell", "Initial", + "Statement", "Switch", + "Property", "Assign", "Assert", "Assume", "Cover", + "ValueKey", "ValueDict", "ValueSet", "SignalKey", "SignalDict", "SignalSet", ] class DUID: - """Deterministic Unique IDentifier""" + """Deterministic Unique IDentifier.""" __next_uid = 0 def __init__(self): self.duid = DUID.__next_uid DUID.__next_uid += 1 +class Shape: + """Bit width and signedness of a value. + + A ``Shape`` can be constructed using: + * explicit bit width and signedness; + * aliases :func:`signed` and :func:`unsigned`; + * casting from a variety of objects. + + A ``Shape`` can be cast from: + * an integer, where the integer specifies the bit width; + * a range, where the result is wide enough to represent any element of the range, and is + signed if any element of the range is signed; + * an :class:`Enum` with all integer members or :class:`IntEnum`, where the result is wide + enough to represent any member of the enumeration, and is signed if any member of + the enumeration is signed. + + Parameters + ---------- + width : int + The number of bits in the representation, including the sign bit (if any). + signed : bool + If ``False``, the value is unsigned. If ``True``, the value is signed two's complement. + """ + def __init__(self, width=1, signed=False): + if not isinstance(width, int) or width < 0: + raise TypeError("Width must be a non-negative integer, not {!r}" + .format(width)) + self.width = width + self.signed = signed + + def __iter__(self): + return iter((self.width, self.signed)) + + @staticmethod + def cast(obj, *, src_loc_at=0): + if isinstance(obj, Shape): + return obj + if isinstance(obj, int): + return Shape(obj) + if isinstance(obj, tuple): + width, signed = obj + warnings.warn("instead of `{tuple}`, use `{constructor}({width})`" + .format(constructor="signed" if signed else "unsigned", width=width, + tuple=obj), + DeprecationWarning, stacklevel=2 + src_loc_at) + return Shape(width, signed) + if isinstance(obj, range): + if len(obj) == 0: + return Shape(0, obj.start < 0) + signed = obj.start < 0 or (obj.stop - obj.step) < 0 + width = max(bits_for(obj.start, signed), + bits_for(obj.stop - obj.step, signed)) + return Shape(width, signed) + if isinstance(obj, type) and issubclass(obj, Enum): + min_value = min(member.value for member in obj) + max_value = max(member.value for member in obj) + if not isinstance(min_value, int) or not isinstance(max_value, int): + raise TypeError("Only enumerations with integer values can be used " + "as value shapes") + signed = min_value < 0 or max_value < 0 + width = max(bits_for(min_value, signed), bits_for(max_value, signed)) + return Shape(width, signed) + raise TypeError("Object {!r} cannot be used as value shape".format(obj)) + + def __repr__(self): + if self.signed: + return "signed({})".format(self.width) + else: + return "unsigned({})".format(self.width) + + def __eq__(self, other): + if isinstance(other, tuple) and len(other) == 2: + width, signed = other + if isinstance(width, int) and isinstance(signed, bool): + return self.width == width and self.signed == signed + else: + raise TypeError("Shapes may be compared with other Shapes and (int, bool) tuples, " + "not {!r}" + .format(other)) + if not isinstance(other, Shape): + raise TypeError("Shapes may be compared with other Shapes and (int, bool) tuples, " + "not {!r}" + .format(other)) + return self.width == other.width and self.signed == other.signed + + +def unsigned(width): + """Shorthand for ``Shape(width, signed=False)``.""" + return Shape(width, signed=False) + + +def signed(width): + """Shorthand for ``Shape(width, signed=True)``.""" + return Shape(width, signed=True) + + class Value(metaclass=ABCMeta): @staticmethod - def wrap(obj): - """Ensures that the passed object is an nMigen value. Booleans and integers - are automatically wrapped into ``Const``.""" + def cast(obj): + """Converts ``obj`` to an nMigen value. + + Booleans and integers are wrapped into a :class:`Const`. Enumerations whose members are + all integers are converted to a :class:`Const` with a shape that fits every member. + """ if isinstance(obj, Value): return obj - elif isinstance(obj, (bool, int)): + if isinstance(obj, int): return Const(obj) - else: - raise TypeError("Object '{!r}' is not an nMigen value".format(obj)) + if isinstance(obj, Enum): + return Const(obj.value, Shape.cast(type(obj))) + if isinstance(obj, ValueCastable): + return obj.as_value() + raise TypeError("Object {!r} cannot be converted to an nMigen value".format(obj)) - def __init__(self, src_loc_at=0): + def __init__(self, *, src_loc_at=0): super().__init__() self.src_loc = tracer.get_src_loc(1 + src_loc_at) def __bool__(self): - raise TypeError("Attempted to convert nMigen value to boolean") + raise TypeError("Attempted to convert nMigen value to Python boolean") def __invert__(self): return Operator("~", [self]) @@ -61,26 +168,56 @@ class Value(metaclass=ABCMeta): return Operator("-", [self, other]) def __rsub__(self, other): return Operator("-", [other, self]) + def __mul__(self, other): return Operator("*", [self, other]) def __rmul__(self, other): return Operator("*", [other, self]) + + def __check_divisor(self): + width, signed = self.shape() + if signed: + # Python's division semantics and Verilog's division semantics differ for negative + # divisors (Python uses div/mod, Verilog uses quo/rem); for now, avoid the issue + # completely by prohibiting such division operations. + raise NotImplementedError("Division by a signed value is not supported") def __mod__(self, other): + other = Value.cast(other) + other.__check_divisor() return Operator("%", [self, other]) def __rmod__(self, other): + self.__check_divisor() return Operator("%", [other, self]) - def __div__(self, other): - return Operator("/", [self, other]) - def __rdiv__(self, other): - return Operator("/", [other, self]) + def __floordiv__(self, other): + other = Value.cast(other) + other.__check_divisor() + return Operator("//", [self, other]) + def __rfloordiv__(self, other): + self.__check_divisor() + return Operator("//", [other, self]) + + def __check_shamt(self): + width, signed = self.shape() + if signed: + # Neither Python nor HDLs implement shifts by negative values; prohibit any shifts + # by a signed value to make sure the shift amount can always be interpreted as + # an unsigned value. + raise TypeError("Shift amount must be unsigned") def __lshift__(self, other): + other = Value.cast(other) + other.__check_shamt() return Operator("<<", [self, other]) def __rlshift__(self, other): + self.__check_shamt() return Operator("<<", [other, self]) def __rshift__(self, other): + other = Value.cast(other) + other.__check_shamt() return Operator(">>", [self, other]) def __rrshift__(self, other): + self.__check_shamt() return Operator(">>", [other, self]) + def __and__(self, other): return Operator("&", [self, other]) def __rand__(self, other): @@ -107,14 +244,21 @@ class Value(metaclass=ABCMeta): def __ge__(self, other): return Operator(">=", [self, other]) + def __abs__(self): + width, signed = self.shape() + if signed: + return Mux(self >= 0, self, -self) + else: + return self + def __len__(self): - return self.shape()[0] + return self.shape().width def __getitem__(self, key): n = len(self) if isinstance(key, int): if key not in range(-n, n): - raise IndexError("Cannot index {} bits into {}-bit value".format(key, n)) + raise IndexError(f"Index {key} is out of bounds for a {n}-bit value") if key < 0: key += n return Slice(self, key, key + 1) @@ -126,16 +270,66 @@ class Value(metaclass=ABCMeta): else: raise TypeError("Cannot index value with {}".format(repr(key))) + def as_unsigned(self): + """Conversion to unsigned. + + Returns + ------- + Value, out + This ``Value`` reinterpreted as a unsigned integer. + """ + return Operator("u", [self]) + + def as_signed(self): + """Conversion to signed. + + Returns + ------- + Value, out + This ``Value`` reinterpreted as a signed integer. + """ + return Operator("s", [self]) + def bool(self): """Conversion to boolean. Returns ------- Value, out - Output ``Value``. If any bits are set, returns ``1``, else ``0``. + ``1`` if any bits are set, ``0`` otherwise. """ return Operator("b", [self]) + def any(self): + """Check if any bits are ``1``. + + Returns + ------- + Value, out + ``1`` if any bits are set, ``0`` otherwise. + """ + return Operator("r|", [self]) + + def all(self): + """Check if all bits are ``1``. + + Returns + ------- + Value, out + ``1`` if all bits are set, ``0`` otherwise. + """ + return Operator("r&", [self]) + + def xor(self): + """Compute pairwise exclusive-or of every bit. + + Returns + ------- + Value, out + ``1`` if an odd number of bits are set, ``0`` if an even number of bits are set. + """ + return Operator("r^", [self]) + def implies(premise, conclusion): """Implication. @@ -146,24 +340,186 @@ class Value(metaclass=ABCMeta): """ return ~premise | conclusion - def part(self, offset, width): - """Indexed part-select. + def bit_select(self, offset, width): + """Part-select with bit granularity. + + Selects a constant width but variable offset part of a ``Value``, such that successive + parts overlap by all but 1 bit. + + Parameters + ---------- + offset : Value, int + Index of first selected bit. + width : int + Number of selected bits. + + Returns + ------- + Part, out + Selected part of the ``Value`` + """ + offset = Value.cast(offset) + if type(offset) is Const and isinstance(width, int): + return self[offset.value:offset.value + width] + return Part(self, offset, width, stride=1, src_loc_at=1) + + def word_select(self, offset, width): + """Part-select with word granularity. - Selects a constant width but variable offset part of a ``Value``. + Selects a constant width but variable offset part of a ``Value``, such that successive + parts do not overlap. Parameters ---------- - offset : Value, in - start point of the selected bits + offset : Value, int + Index of first selected word. width : int - number of selected bits + Number of selected bits. Returns ------- Part, out Selected part of the ``Value`` """ - return Part(self, offset, width) + offset = Value.cast(offset) + if type(offset) is Const and isinstance(width, int): + return self[offset.value * width:(offset.value + 1) * width] + return Part(self, offset, width, stride=width, src_loc_at=1) + + def matches(self, *patterns): + """Pattern matching. + + Matches against a set of patterns, which may be integers or bit strings, recognizing + the same grammar as ``Case()``. + + Parameters + ---------- + patterns : int or str + Patterns to match against. + + Returns + ------- + Value, out + ``1`` if any pattern matches the value, ``0`` otherwise. + """ + matches = [] + for pattern in patterns: + if not isinstance(pattern, (int, str, Enum)): + raise SyntaxError("Match pattern must be an integer, a string, or an enumeration, " + "not {!r}" + .format(pattern)) + if isinstance(pattern, str) and any(bit not in "01- \t" for bit in pattern): + raise SyntaxError("Match pattern '{}' must consist of 0, 1, and - (don't care) " + "bits, and may include whitespace" + .format(pattern)) + if (isinstance(pattern, str) and + len("".join(pattern.split())) != len(self)): + raise SyntaxError("Match pattern '{}' must have the same width as match value " + "(which is {})" + .format(pattern, len(self))) + if isinstance(pattern, int) and bits_for(pattern) > len(self): + warnings.warn("Match pattern '{:b}' is wider than match value " + "(which has width {}); comparison will never be true" + .format(pattern, len(self)), + SyntaxWarning, stacklevel=3) + continue + if isinstance(pattern, str): + pattern = "".join(pattern.split()) # remove whitespace + mask = int(pattern.replace("0", "1").replace("-", "0"), 2) + pattern = int(pattern.replace("-", "0"), 2) + matches.append((self & mask) == pattern) + elif isinstance(pattern, int): + matches.append(self == pattern) + elif isinstance(pattern, Enum): + matches.append(self == pattern.value) + else: + assert False + if not matches: + return Const(0) + elif len(matches) == 1: + return matches[0] + else: + return Cat(*matches).any() + + def shift_left(self, amount): + """Shift left by constant amount. + + Parameters + ---------- + amount : int + Amount to shift by. + + Returns + ------- + Value, out + If the amount is positive, the input shifted left. Otherwise, the input shifted right. + """ + if not isinstance(amount, int): + raise TypeError("Shift amount must be an integer, not {!r}".format(amount)) + if amount < 0: + return self.shift_right(-amount) + if self.shape().signed: + return Cat(Const(0, amount), self).as_signed() + else: + return Cat(Const(0, amount), self) # unsigned + + def shift_right(self, amount): + """Shift right by constant amount. + + Parameters + ---------- + amount : int + Amount to shift by. + + Returns + ------- + Value, out + If the amount is positive, the input shifted right. Otherwise, the input shifted left. + """ + if not isinstance(amount, int): + raise TypeError("Shift amount must be an integer, not {!r}".format(amount)) + if amount < 0: + return self.shift_left(-amount) + if self.shape().signed: + return self[amount:].as_signed() + else: + return self[amount:] # unsigned + + def rotate_left(self, amount): + """Rotate left by constant amount. + + Parameters + ---------- + amount : int + Amount to rotate by. + + Returns + ------- + Value, out + If the amount is positive, the input rotated left. Otherwise, the input rotated right. + """ + if not isinstance(amount, int): + raise TypeError("Rotate amount must be an integer, not {!r}".format(amount)) + amount %= len(self) + return Cat(self[-amount:], self[:-amount]) # meow :3 + + def rotate_right(self, amount): + """Rotate right by constant amount. + + Parameters + ---------- + amount : int + Amount to rotate by. + + Returns + ------- + Value, out + If the amount is positive, the input rotated right. Otherwise, the input rotated right. + """ + if not isinstance(amount, int): + raise TypeError("Rotate amount must be an integer, not {!r}".format(amount)) + amount %= len(self) + return Cat(self[amount:], self[:amount]) def eq(self, value): """Assignment. @@ -182,20 +538,19 @@ class Value(metaclass=ABCMeta): @abstractmethod def shape(self): - """Bit length and signedness of a value. + """Bit width and signedness of a value. Returns ------- - int, bool - Number of bits required to store `v` or available in `v`, followed by - whether `v` has a sign bit (included in the bit count). + Shape + See :class:`Shape`. Examples -------- - >>> Value.shape(Signal(8)) - 8, False - >>> Value.shape(C(0xaa)) - 8, False + >>> Signal(8).shape() + Shape(width=8, signed=False) + >>> Const(0xaa).shape() + Shape(width=8, signed=False) """ pass # :nocov: @@ -220,158 +575,156 @@ class Const(Value): ---------- value : int shape : int or tuple or None - Either an integer `bits` or a tuple `(bits, signed)` - specifying the number of bits in this `Const` and whether it is - signed (can represent negative values). `shape` defaults - to the minimum width and signedness of `value`. + Either an integer ``width`` or a tuple ``(width, signed)`` specifying the number of bits + in this constant and whether it is signed (can represent negative values). + ``shape`` defaults to the minimum possible width and signedness of ``value``. Attributes ---------- - nbits : int + width : int signed : bool """ src_loc = None @staticmethod def normalize(value, shape): - nbits, signed = shape - mask = (1 << nbits) - 1 + width, signed = shape + mask = (1 << width) - 1 value &= mask - if signed and value >> (nbits - 1): + if signed and value >> (width - 1): value |= ~mask return value - def __init__(self, value, shape=None): + def __init__(self, value, shape=None, *, src_loc_at=0): + # We deliberately do not call Value.__init__ here. self.value = int(value) if shape is None: - shape = bits_for(self.value), self.value < 0 - if isinstance(shape, int): - shape = shape, self.value < 0 - self.nbits, self.signed = shape - if not isinstance(self.nbits, int) or self.nbits < 0: - raise TypeError("Width must be a non-negative integer, not '{!r}'" - .format(self.nbits)) + shape = Shape(bits_for(self.value), signed=self.value < 0) + elif isinstance(shape, int): + shape = Shape(shape, signed=self.value < 0) + else: + shape = Shape.cast(shape, src_loc_at=1 + src_loc_at) + self.width, self.signed = shape self.value = self.normalize(self.value, shape) def shape(self): - return self.nbits, self.signed + return Shape(self.width, self.signed) def _rhs_signals(self): - return ValueSet() + return SignalSet() def _as_const(self): return self.value def __repr__(self): - return "(const {}'{}d{})".format(self.nbits, "s" if self.signed else "", self.value) + return "(const {}'{}d{})".format(self.width, "s" if self.signed else "", self.value) C = Const # shorthand class AnyValue(Value, DUID): - def __init__(self, shape): - super().__init__(src_loc_at=0) - if isinstance(shape, int): - shape = shape, False - self.nbits, self.signed = shape - if not isinstance(self.nbits, int) or self.nbits < 0: - raise TypeError("Width must be a non-negative integer, not '{!r}'" - .format(self.nbits)) + def __init__(self, shape, *, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) + self.width, self.signed = Shape.cast(shape, src_loc_at=1 + src_loc_at) + if not isinstance(self.width, int) or self.width < 0: + raise TypeError("Width must be a non-negative integer, not {!r}" + .format(self.width)) def shape(self): - return self.nbits, self.signed + return Shape(self.width, self.signed) def _rhs_signals(self): - return ValueSet() + return SignalSet() @final class AnyConst(AnyValue): def __repr__(self): - return "(anyconst {}'{})".format(self.nbits, "s" if self.signed else "") + return "(anyconst {}'{})".format(self.width, "s" if self.signed else "") @final class AnySeq(AnyValue): def __repr__(self): - return "(anyseq {}'{})".format(self.nbits, "s" if self.signed else "") + return "(anyseq {}'{})".format(self.width, "s" if self.signed else "") @final class Operator(Value): - def __init__(self, op, operands, src_loc_at=0): + def __init__(self, operator, operands, *, src_loc_at=0): super().__init__(src_loc_at=1 + src_loc_at) - self.op = op - self.operands = [Value.wrap(o) for o in operands] - - @staticmethod - def _bitwise_binary_shape(a_shape, b_shape): - a_bits, a_sign = a_shape - b_bits, b_sign = b_shape - if not a_sign and not b_sign: - # both operands unsigned - return max(a_bits, b_bits), False - elif a_sign and b_sign: - # both operands signed - return max(a_bits, b_bits), True - elif not a_sign and b_sign: - # first operand unsigned (add sign bit), second operand signed - return max(a_bits + 1, b_bits), True - else: - # first signed, second operand unsigned (add sign bit) - return max(a_bits, b_bits + 1), True + self.operator = operator + self.operands = [Value.cast(op) for op in operands] def shape(self): + def _bitwise_binary_shape(a_shape, b_shape): + a_bits, a_sign = a_shape + b_bits, b_sign = b_shape + if not a_sign and not b_sign: + # both operands unsigned + return Shape(max(a_bits, b_bits), False) + elif a_sign and b_sign: + # both operands signed + return Shape(max(a_bits, b_bits), True) + elif not a_sign and b_sign: + # first operand unsigned (add sign bit), second operand signed + return Shape(max(a_bits + 1, b_bits), True) + else: + # first signed, second operand unsigned (add sign bit) + return Shape(max(a_bits, b_bits + 1), True) + op_shapes = list(map(lambda x: x.shape(), self.operands)) if len(op_shapes) == 1: - (a_bits, a_sign), = op_shapes - if self.op in ("+", "~"): - return a_bits, a_sign - if self.op == "-": - if not a_sign: - return a_bits + 1, True - else: - return a_bits, a_sign - if self.op == "b": - return 1, False + (a_width, a_signed), = op_shapes + if self.operator in ("+", "~"): + return Shape(a_width, a_signed) + if self.operator == "-": + return Shape(a_width + 1, True) + if self.operator in ("b", "r|", "r&", "r^"): + return Shape(1, False) + if self.operator == "u": + return Shape(a_width, False) + if self.operator == "s": + return Shape(a_width, True) elif len(op_shapes) == 2: - (a_bits, a_sign), (b_bits, b_sign) = op_shapes - if self.op == "+" or self.op == "-": - bits, sign = self._bitwise_binary_shape(*op_shapes) - return bits + 1, sign - if self.op == "*": - return a_bits + b_bits, a_sign or b_sign - if self.op == "%": - return a_bits, a_sign - if self.op in ("<", "<=", "==", "!=", ">", ">=", "b"): - return 1, False - if self.op in ("&", "^", "|"): - return self._bitwise_binary_shape(*op_shapes) - if self.op == "<<": - if b_sign: - extra = 2 ** (b_bits - 1) - 1 + (a_width, a_signed), (b_width, b_signed) = op_shapes + if self.operator in ("+", "-"): + width, signed = _bitwise_binary_shape(*op_shapes) + return Shape(width + 1, signed) + if self.operator == "*": + return Shape(a_width + b_width, a_signed or b_signed) + if self.operator in ("//", "%"): + assert not b_signed + return Shape(a_width, a_signed) + if self.operator in ("<", "<=", "==", "!=", ">", ">="): + return Shape(1, False) + if self.operator in ("&", "^", "|"): + return _bitwise_binary_shape(*op_shapes) + if self.operator == "<<": + if b_signed: + extra = 2 ** (b_width - 1) - 1 else: - extra = 2 ** (b_bits) - 1 - return a_bits + extra, a_sign - if self.op == ">>": - if b_sign: - extra = 2 ** (b_bits - 1) + extra = 2 ** (b_width) - 1 + return Shape(a_width + extra, a_signed) + if self.operator == ">>": + if b_signed: + extra = 2 ** (b_width - 1) else: extra = 0 - return a_bits + extra, a_sign + return Shape(a_width + extra, a_signed) elif len(op_shapes) == 3: - if self.op == "m": + if self.operator == "m": s_shape, a_shape, b_shape = op_shapes - return self._bitwise_binary_shape(a_shape, b_shape) + return _bitwise_binary_shape(a_shape, b_shape) raise NotImplementedError("Operator {}/{} not implemented" - .format(self.op, len(op_shapes))) # :nocov: + .format(self.operator, len(op_shapes))) # :nocov: def _rhs_signals(self): return union(op._rhs_signals() for op in self.operands) def __repr__(self): - return "({} {})".format(self.op, " ".join(map(repr, self.operands))) + return "({} {})".format(self.operator, " ".join(map(repr, self.operands))) def Mux(sel, val1, val0): @@ -390,36 +743,39 @@ def Mux(sel, val1, val0): Value, out Output ``Value``. If ``sel`` is asserted, the Mux returns ``val1``, else ``val0``. """ + sel = Value.cast(sel) + if len(sel) != 1: + sel = sel.bool() return Operator("m", [sel, val1, val0]) @final class Slice(Value): - def __init__(self, value, start, end): + def __init__(self, value, start, stop, *, src_loc_at=0): if not isinstance(start, int): - raise TypeError("Slice start must be an integer, not '{!r}'".format(start)) - if not isinstance(end, int): - raise TypeError("Slice end must be an integer, not '{!r}'".format(end)) + raise TypeError("Slice start must be an integer, not {!r}".format(start)) + if not isinstance(stop, int): + raise TypeError("Slice stop must be an integer, not {!r}".format(stop)) n = len(value) if start not in range(-(n+1), n+1): raise IndexError("Cannot start slice {} bits into {}-bit value".format(start, n)) if start < 0: start += n - if end not in range(-(n+1), n+1): - raise IndexError("Cannot end slice {} bits into {}-bit value".format(end, n)) - if end < 0: - end += n - if start > end: - raise IndexError("Slice start {} must be less than slice end {}".format(start, end)) + if stop not in range(-(n+1), n+1): + raise IndexError("Cannot stop slice {} bits into {}-bit value".format(stop, n)) + if stop < 0: + stop += n + if start > stop: + raise IndexError("Slice start {} must be less than slice stop {}".format(start, stop)) - super().__init__() - self.value = Value.wrap(value) + super().__init__(src_loc_at=src_loc_at) + self.value = Value.cast(value) self.start = start - self.end = end + self.stop = stop def shape(self): - return self.end - self.start, False + return Shape(self.stop - self.start) def _lhs_signals(self): return self.value._lhs_signals() @@ -428,22 +784,25 @@ class Slice(Value): return self.value._rhs_signals() def __repr__(self): - return "(slice {} {}:{})".format(repr(self.value), self.start, self.end) + return "(slice {} {}:{})".format(repr(self.value), self.start, self.stop) @final class Part(Value): - def __init__(self, value, offset, width): + def __init__(self, value, offset, width, stride=1, *, src_loc_at=0): if not isinstance(width, int) or width < 0: - raise TypeError("Part width must be a non-negative integer, not '{!r}'".format(width)) + raise TypeError("Part width must be a non-negative integer, not {!r}".format(width)) + if not isinstance(stride, int) or stride <= 0: + raise TypeError("Part stride must be a positive integer, not {!r}".format(stride)) - super().__init__() + super().__init__(src_loc_at=src_loc_at) self.value = value - self.offset = Value.wrap(offset) + self.offset = Value.cast(offset) self.width = width + self.stride = stride def shape(self): - return self.width, False + return Shape(self.width) def _lhs_signals(self): return self.value._lhs_signals() @@ -452,7 +811,8 @@ class Part(Value): return self.value._rhs_signals() | self.offset._rhs_signals() def __repr__(self): - return "(part {} {} {})".format(repr(self.value), repr(self.offset), self.width) + return "(part {} {} {} {})".format(repr(self.value), repr(self.offset), + self.width, self.stride) @final @@ -480,18 +840,18 @@ class Cat(Value): Value, inout Resulting ``Value`` obtained by concatentation. """ - def __init__(self, *args): - super().__init__() - self.parts = [Value.wrap(v) for v in flatten(args)] + def __init__(self, *args, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) + self.parts = [Value.cast(v) for v in flatten(args)] def shape(self): - return sum(len(part) for part in self.parts), False + return Shape(sum(len(part) for part in self.parts)) def _lhs_signals(self): - return union((part._lhs_signals() for part in self.parts), start=ValueSet()) + return union((part._lhs_signals() for part in self.parts), start=SignalSet()) def _rhs_signals(self): - return union((part._rhs_signals() for part in self.parts), start=ValueSet()) + return union((part._rhs_signals() for part in self.parts), start=SignalSet()) def _as_const(self): value = 0 @@ -525,17 +885,17 @@ class Repl(Value): Repl, out Replicated value. """ - def __init__(self, value, count): + def __init__(self, value, count, *, src_loc_at=0): if not isinstance(count, int) or count < 0: - raise TypeError("Replication count must be a non-negative integer, not '{!r}'" + raise TypeError("Replication count must be a non-negative integer, not {!r}" .format(count)) - super().__init__() - self.value = Value.wrap(value) + super().__init__(src_loc_at=src_loc_at) + self.value = Value.cast(value) self.count = count def shape(self): - return len(self.value) * self.count, False + return Shape(len(self.value) * self.count) def _rhs_signals(self): return self.value._rhs_signals() @@ -544,22 +904,20 @@ class Repl(Value): return "(repl {!r} {})".format(self.value, self.count) -@final +# @final class Signal(Value, DUID): """A varying integer value. Parameters ---------- - shape : int or tuple or None - Either an integer ``bits`` or a tuple ``(bits, signed)`` specifying the number of bits - in this ``Signal`` and whether it is signed (can represent negative values). - ``shape`` defaults to 1-bit and non-signed. + shape : ``Shape``-castable object or None + Specification for the number of bits in this ``Signal`` and its signedness (whether it + can represent negative values). See ``Shape.cast`` for details. + If not specified, ``shape`` defaults to 1-bit and non-signed. name : str Name hint for this signal. If ``None`` (default) the name is inferred from the variable - name this ``Signal`` is assigned to. Name collisions are automatically resolved by - prepending names of objects that contain this ``Signal`` and by appending integer - sequences. - reset : int + name this ``Signal`` is assigned to. + reset : int or integral Enum Reset (synchronous) or default (combinatorial) value. When this ``Signal`` is assigned to in synchronous context and the corresponding clock domain is reset, the ``Signal`` assumes the given value. When this ``Signal`` is unassigned @@ -569,11 +927,6 @@ class Signal(Value, DUID): If ``True``, do not generate reset logic for this ``Signal`` in synchronous statements. The ``reset`` value is only used as a combinatorial default or as the initial value. Defaults to ``False``. - min : int or None - max : int or None - If ``shape`` is ``None``, the signal bit width and signedness are - determined by the integer range given by ``min`` (inclusive, - defaults to 0) and ``max`` (exclusive, defaults to 2). attrs : dict Dictionary of synthesis attributes. decoder : function or Enum @@ -584,52 +937,46 @@ class Signal(Value, DUID): Attributes ---------- - nbits : int + width : int signed : bool name : str reset : int reset_less : bool attrs : dict + decoder : function """ - def __init__(self, shape=None, name=None, reset=0, reset_less=False, min=None, max=None, + def __init__(self, shape=None, *, name=None, reset=0, reset_less=False, attrs=None, decoder=None, src_loc_at=0): super().__init__(src_loc_at=src_loc_at) if name is not None and not isinstance(name, str): - raise TypeError("Name must be a string, not '{!r}'".format(name)) + raise TypeError("Name must be a string, not {!r}".format(name)) self.name = name or tracer.get_var_name(depth=2 + src_loc_at, default="$signal") if shape is None: - if min is None: - min = 0 - if max is None: - max = 2 - max -= 1 # make both bounds inclusive - if min > max: - raise ValueError("Lower bound {} should be less or equal to higher bound {}" - .format(min, max + 1)) - self.signed = min < 0 or max < 0 - if min == max: - self.nbits = 0 - else: - self.nbits = builtins.max(bits_for(min, self.signed), - bits_for(max, self.signed)) - - else: - if not (min is None and max is None): - raise ValueError("Only one of bits/signedness or bounds may be specified") - if isinstance(shape, int): - self.nbits, self.signed = shape, False - else: - self.nbits, self.signed = shape - - if not isinstance(self.nbits, int) or self.nbits < 0: - raise TypeError("Width must be a non-negative integer, not '{!r}'".format(self.nbits)) - self.reset = int(reset) + shape = unsigned(1) + self.width, self.signed = Shape.cast(shape, src_loc_at=1 + src_loc_at) + + if isinstance(reset, Enum): + reset = reset.value + if not isinstance(reset, int): + raise TypeError("Reset value has to be an int or an integral Enum") + + reset_width = bits_for(reset, self.signed) + if reset != 0 and reset_width > self.width: + warnings.warn("Reset value {!r} requires {} bits to represent, but the signal " + "only has {} bits" + .format(reset, reset_width, self.width), + SyntaxWarning, stacklevel=2 + src_loc_at) + + self.reset = reset self.reset_less = bool(reset_less) self.attrs = OrderedDict(() if attrs is None else attrs) + + if decoder is None and isinstance(shape, type) and issubclass(shape, Enum): + decoder = shape if isinstance(decoder, type) and issubclass(decoder, Enum): def enum_decoder(value): try: @@ -637,11 +984,14 @@ class Signal(Value, DUID): except ValueError: return str(value) self.decoder = enum_decoder + self._enum_class = decoder else: self.decoder = decoder + self._enum_class = None - @classmethod - def like(cls, other, name=None, name_suffix=None, src_loc_at=0, **kwargs): + # Not a @classmethod because nmigen.compat requires it. + @staticmethod + def like(other, *, name=None, name_suffix=None, src_loc_at=0, **kwargs): """Create Signal based on another. Parameters @@ -655,21 +1005,21 @@ class Signal(Value, DUID): new_name = other.name + str(name_suffix) else: new_name = tracer.get_var_name(depth=2 + src_loc_at, default="$like") - kw = dict(shape=cls.wrap(other).shape(), name=new_name) - if isinstance(other, cls): + kw = dict(shape=Value.cast(other).shape(), name=new_name) + if isinstance(other, Signal): kw.update(reset=other.reset, reset_less=other.reset_less, attrs=other.attrs, decoder=other.decoder) kw.update(kwargs) - return cls(**kw, src_loc_at=1 + src_loc_at) + return Signal(**kw, src_loc_at=1 + src_loc_at) def shape(self): - return self.nbits, self.signed + return Shape(self.width, self.signed) def _lhs_signals(self): - return ValueSet((self,)) + return SignalSet((self,)) def _rhs_signals(self): - return ValueSet((self,)) + return SignalSet((self,)) def __repr__(self): return "(sig {})".format(self.name) @@ -688,17 +1038,19 @@ class ClockSignal(Value): domain : str Clock domain to obtain a clock signal for. Defaults to ``"sync"``. """ - def __init__(self, domain="sync"): - super().__init__() + def __init__(self, domain="sync", *, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) if not isinstance(domain, str): - raise TypeError("Clock domain name must be a string, not '{!r}'".format(domain)) + raise TypeError("Clock domain name must be a string, not {!r}".format(domain)) + if domain == "comb": + raise ValueError("Domain '{}' does not have a clock".format(domain)) self.domain = domain def shape(self): - return 1, False + return Shape(1) def _lhs_signals(self): - return ValueSet((self,)) + return SignalSet((self,)) def _rhs_signals(self): raise NotImplementedError("ClockSignal must be lowered to a concrete signal") # :nocov: @@ -722,18 +1074,20 @@ class ResetSignal(Value): allow_reset_less : bool If the clock domain is reset-less, act as a constant ``0`` instead of reporting an error. """ - def __init__(self, domain="sync", allow_reset_less=False): - super().__init__() + def __init__(self, domain="sync", allow_reset_less=False, *, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) if not isinstance(domain, str): - raise TypeError("Clock domain name must be a string, not '{!r}'".format(domain)) + raise TypeError("Clock domain name must be a string, not {!r}".format(domain)) + if domain == "comb": + raise ValueError("Domain '{}' does not have a reset".format(domain)) self.domain = domain self.allow_reset_less = allow_reset_less def shape(self): - return 1, False + return Shape(1) def _lhs_signals(self): - return ValueSet((self,)) + return SignalSet((self,)) def _rhs_signals(self): raise NotImplementedError("ResetSignal must be lowered to a concrete signal") # :nocov: @@ -766,29 +1120,29 @@ class Array(MutableSequence): gpios = Array(Signal() for _ in range(10)) with m.If(bus.we): - m.d.sync += gpios[bus.adr].eq(bus.dat_w) + m.d.sync += gpios[bus.addr].eq(bus.w_data) with m.Else(): - m.d.sync += bus.dat_r.eq(gpios[bus.adr]) + m.d.sync += bus.r_data.eq(gpios[bus.addr]) Multidimensional array:: mult = Array(Array(x * y for y in range(10)) for x in range(10)) - a = Signal(max=10) - b = Signal(max=10) + a = Signal.range(10) + b = Signal.range(10) r = Signal(8) m.d.comb += r.eq(mult[a][b]) Array of records:: layout = [ - ("re", 1), - ("dat_r", 16), + ("r_data", 16), + ("r_en", 1), ] buses = Array(Record(layout) for busno in range(4)) master = Record(layout) m.d.comb += [ - buses[sel].re.eq(master.re), - master.dat_r.eq(buses[sel].dat_r), + buses[sel].r_en.eq(master.r_en), + master.r_data.eq(buses[sel].r_data), ] """ def __init__(self, iterable=()): @@ -832,10 +1186,10 @@ class Array(MutableSequence): @final class ArrayProxy(Value): - def __init__(self, elems, index): - super().__init__(src_loc_at=1) + def __init__(self, elems, index, *, src_loc_at=0): + super().__init__(src_loc_at=1 + src_loc_at) self.elems = elems - self.index = Value.wrap(index) + self.index = Value.cast(index) def __getattr__(self, attr): return ArrayProxy([getattr(elem, attr) for elem in self.elems], self.index) @@ -844,21 +1198,39 @@ class ArrayProxy(Value): return ArrayProxy([ elem[index] for elem in self.elems], self.index) def _iter_as_values(self): - return (Value.wrap(elem) for elem in self.elems) + return (Value.cast(elem) for elem in self.elems) def shape(self): - bits, sign = 0, False - for elem_bits, elem_sign in (elem.shape() for elem in self._iter_as_values()): - bits = max(bits, elem_bits + elem_sign) - sign = max(sign, elem_sign) - return bits, sign + unsigned_width = signed_width = 0 + has_unsigned = has_signed = False + for elem_width, elem_signed in (elem.shape() for elem in self._iter_as_values()): + if elem_signed: + has_signed = True + signed_width = max(signed_width, elem_width) + else: + has_unsigned = True + unsigned_width = max(unsigned_width, elem_width) + # The shape of the proxy must be such that it preserves the mathematical value of the array + # elements. I.e., shape-wise, an array proxy must be identical to an equivalent mux tree. + # To ensure this holds, if the array contains both signed and unsigned values, make sure + # that every unsigned value is zero-extended by at least one bit. + if has_signed and has_unsigned and unsigned_width >= signed_width: + # Array contains both signed and unsigned values, and at least one of the unsigned + # values won't be zero-extended otherwise. + return signed(unsigned_width + 1) + else: + # Array contains values of the same signedness, or else all of the unsigned values + # are zero-extended. + return Shape(max(unsigned_width, signed_width), has_signed) def _lhs_signals(self): - signals = union((elem._lhs_signals() for elem in self._iter_as_values()), start=ValueSet()) + signals = union((elem._lhs_signals() for elem in self._iter_as_values()), + start=SignalSet()) return signals def _rhs_signals(self): - signals = union((elem._rhs_signals() for elem in self._iter_as_values()), start=ValueSet()) + signals = union((elem._rhs_signals() for elem in self._iter_as_values()), + start=SignalSet()) return self.index._rhs_signals() | signals def __repr__(self): @@ -885,7 +1257,7 @@ class UserValue(Value): * Indexing or iterating through individual bits; * Adding an assignment to the value to a ``Module`` using ``m.d. +=``. """ - def __init__(self, src_loc_at=1): + def __init__(self, *, src_loc_at=0): super().__init__(src_loc_at=1 + src_loc_at) self.__lowered = None @@ -896,7 +1268,10 @@ class UserValue(Value): def _lazy_lower(self): if self.__lowered is None: - self.__lowered = Value.wrap(self.lower()) + lowered = self.lower() + if isinstance(lowered, UserValue): + lowered = lowered._lazy_lower() + self.__lowered = Value.cast(lowered) return self.__lowered def shape(self): @@ -909,6 +1284,51 @@ class UserValue(Value): return self._lazy_lower()._rhs_signals() +class ValueCastable: + """Base class for classes which can be cast to Values. + + A ``ValueCastable`` can be cast to ``Value``, meaning its precise representation does not have + to be immediately known. This is useful in certain metaprogramming scenarios. Instead of + providing fixed semantics upfront, it is kept abstract for as long as possible, only being + cast to a concrete nMigen value when required. + + Note that it is necessary to ensure that nMigen's view of representation of all values stays + internally consistent. The class deriving from ``ValueCastable`` must decorate the ``as_value`` + method with the ``lowermethod`` decorator, which ensures that all calls to ``as_value``return the + same ``Value`` representation. If the class deriving from ``ValueCastable`` is mutable, it is + up to the user to ensure that it is not mutated in a way that changes its representation after + the first call to ``as_value``. + """ + def __new__(cls, *args, **kwargs): + self = super().__new__(cls) + if not hasattr(self, "as_value"): + raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must override the `as_value` method") + + if not hasattr(self.as_value, "_ValueCastable__memoized"): + raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must decorate the `as_value` " + "method with the `ValueCastable.lowermethod` decorator") + return self + + @staticmethod + def lowermethod(func): + """Decorator to memoize lowering methods. + + Ensures the decorated method is called only once, with subsequent method calls returning the + object returned by the first first method call. + + This decorator is required to decorate the ``as_value`` method of ``ValueCastable`` subclasses. + This is to ensure that nMigen's view of representation of all values stays internally + consistent. + """ + @functools.wraps(func) + def wrapper_memoized(self, *args, **kwargs): + if not hasattr(self, "_ValueCastable__lowered_to"): + self.__lowered_to = func(self, *args, **kwargs) + return self.__lowered_to + wrapper_memoized.__memoized = True + return wrapper_memoized + + @final class Sample(Value): """Value from the past. @@ -917,23 +1337,26 @@ class Sample(Value): of the ``domain`` clock back. If that moment is before the beginning of time, it is equal to the value of the expression calculated as if each signal had its reset value. """ - def __init__(self, expr, clocks, domain): - super().__init__(src_loc_at=1) - self.value = Value.wrap(expr) + def __init__(self, expr, clocks, domain, *, src_loc_at=0): + super().__init__(src_loc_at=1 + src_loc_at) + self.value = Value.cast(expr) self.clocks = int(clocks) self.domain = domain - if not isinstance(self.value, (Const, Signal, ClockSignal, ResetSignal)): - raise TypeError("Sampled value may only be a signal or a constant, not {!r}" + if not isinstance(self.value, (Const, Signal, ClockSignal, ResetSignal, Initial)): + raise TypeError("Sampled value must be a signal or a constant, not {!r}" .format(self.value)) if self.clocks < 0: raise ValueError("Cannot sample a value {} cycles in the future" .format(-self.clocks)) + if not (self.domain is None or isinstance(self.domain, str)): + raise TypeError("Domain name must be a string or None, not {!r}" + .format(self.domain)) def shape(self): return self.value.shape() def _rhs_signals(self): - return ValueSet((self,)) + return SignalSet((self,)) def __repr__(self): return "(sample {!r} @ {}[{}])".format( @@ -956,30 +1379,51 @@ def Fell(expr, clocks=0, domain=None): return Sample(expr, clocks + 1, domain) & ~Sample(expr, clocks, domain) +@final +class Initial(Value): + """Start indicator, for model checking. + + An ``Initial`` signal is ``1`` at the first cycle of model checking, and ``0`` at any other. + """ + def __init__(self, *, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) + + def shape(self): + return Shape(1) + + def _rhs_signals(self): + return SignalSet((self,)) + + def __repr__(self): + return "(initial)" + + class _StatementList(list): def __repr__(self): return "({})".format(" ".join(map(repr, self))) class Statement: + def __init__(self, *, src_loc_at=0): + self.src_loc = tracer.get_src_loc(1 + src_loc_at) + @staticmethod - def wrap(obj): + def cast(obj): if isinstance(obj, Iterable): - return _StatementList(sum((Statement.wrap(e) for e in obj), [])) + return _StatementList(sum((Statement.cast(e) for e in obj), [])) else: if isinstance(obj, Statement): return _StatementList([obj]) else: - raise TypeError("Object '{!r}' is not an nMigen statement".format(obj)) + raise TypeError("Object {!r} is not an nMigen statement".format(obj)) @final class Assign(Statement): - def __init__(self, lhs, rhs, src_loc_at=0): - self.src_loc = tracer.get_src_loc(src_loc_at) - - self.lhs = Value.wrap(lhs) - self.rhs = Value.wrap(rhs) + def __init__(self, lhs, rhs, *, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) + self.lhs = Value.cast(lhs) + self.rhs = Value.cast(rhs) def _lhs_signals(self): return self.lhs._lhs_signals() @@ -991,24 +1435,27 @@ class Assign(Statement): return "(eq {!r} {!r})".format(self.lhs, self.rhs) -class Property(Statement): - def __init__(self, test, _check=None, _en=None): - self.src_loc = tracer.get_src_loc() +class UnusedProperty(UnusedMustUse): + pass + - self.test = Value.wrap(test) +class Property(Statement, MustUse): + _MustUse__warning = UnusedProperty + def __init__(self, test, *, _check=None, _en=None, src_loc_at=0): + super().__init__(src_loc_at=src_loc_at) + self.test = Value.cast(test) self._check = _check + self._en = _en if self._check is None: self._check = Signal(reset_less=True, name="${}$check".format(self._kind)) self._check.src_loc = self.src_loc - - self._en = _en if _en is None: self._en = Signal(reset_less=True, name="${}$en".format(self._kind)) self._en.src_loc = self.src_loc def _lhs_signals(self): - return ValueSet((self._en, self._check)) + return SignalSet((self._en, self._check)) def _rhs_signals(self): return self.test._rhs_signals() @@ -1027,15 +1474,29 @@ class Assume(Property): _kind = "assume" +@final +class Cover(Property): + _kind = "cover" + + # @final class Switch(Statement): - def __init__(self, test, cases, src_loc_at=0): - self.src_loc = tracer.get_src_loc(src_loc_at) - - self.test = Value.wrap(test) + def __init__(self, test, cases, *, src_loc=None, src_loc_at=0, case_src_locs={}): + if src_loc is None: + super().__init__(src_loc_at=src_loc_at) + else: + # Switch is a bit special in terms of location tracking because it is usually created + # long after the control has left the statement that directly caused its creation. + self.src_loc = src_loc + # Switch is also a bit special in that its parts also have location information. It can't + # be automatically traced, so whatever constructs a Switch may optionally provide it. + self.case_src_locs = {} + + self.test = Value.cast(test) self.cases = OrderedDict() - for keys, stmts in cases.items(): + for orig_keys, stmts in cases.items(): # Map: None -> (); key -> (key,); (key...) -> (key...) + keys = orig_keys if keys is None: keys = () if not isinstance(keys, tuple): @@ -1043,27 +1504,31 @@ class Switch(Statement): # Map: 2 -> "0010"; "0010" -> "0010" new_keys = () for key in keys: - if isinstance(key, (bool, int)): - key = "{:0{}b}".format(key, len(self.test)) - elif isinstance(key, str): - pass + if isinstance(key, str): + key = "".join(key.split()) # remove whitespace + elif isinstance(key, int): + key = format(key, "b").rjust(len(self.test), "0") + elif isinstance(key, Enum): + key = format(key.value, "b").rjust(len(self.test), "0") else: - raise TypeError("Object '{!r}' cannot be used as a switch key" + raise TypeError("Object {!r} cannot be used as a switch key" .format(key)) assert len(key) == len(self.test) new_keys = (*new_keys, key) if not isinstance(stmts, Iterable): stmts = [stmts] - self.cases[new_keys] = Statement.wrap(stmts) + self.cases[new_keys] = Statement.cast(stmts) + if orig_keys in case_src_locs: + self.case_src_locs[new_keys] = case_src_locs[orig_keys] def _lhs_signals(self): signals = union((s._lhs_signals() for ss in self.cases.values() for s in ss), - start=ValueSet()) + start=SignalSet()) return signals def _rhs_signals(self): signals = union((s._rhs_signals() for ss in self.cases.values() for s in ss), - start=ValueSet()) + start=SignalSet()) return self.test._rhs_signals() | signals def __repr__(self): @@ -1079,42 +1544,6 @@ class Switch(Statement): return "(switch {!r} {})".format(self.test, " ".join(case_reprs)) -@final -class Delay(Statement): - def __init__(self, interval=None): - self.interval = None if interval is None else float(interval) - - def _rhs_signals(self): - return ValueSet() - - def __repr__(self): - if self.interval is None: - return "(delay ε)" - else: - return "(delay {:.3}us)".format(self.interval * 1e6) - - -@final -class Tick(Statement): - def __init__(self, domain="sync"): - self.domain = str(domain) - - def _rhs_signals(self): - return ValueSet() - - def __repr__(self): - return "(tick {})".format(self.domain) - - -@final -class Passive(Statement): - def _rhs_signals(self): - return ValueSet() - - def __repr__(self): - return "(passive)" - - class _MappedKeyCollection(metaclass=ABCMeta): @abstractmethod def _map_key(self, key): @@ -1205,33 +1634,37 @@ class _MappedKeySet(MutableSet, _MappedKeyCollection): class ValueKey: def __init__(self, value): - self.value = Value.wrap(value) - - def __hash__(self): + self.value = Value.cast(value) if isinstance(self.value, Const): - return hash(self.value.value) + self._hash = hash(self.value.value) elif isinstance(self.value, (Signal, AnyValue)): - return hash(self.value.duid) + self._hash = hash(self.value.duid) elif isinstance(self.value, (ClockSignal, ResetSignal)): - return hash(self.value.domain) + self._hash = hash(self.value.domain) elif isinstance(self.value, Operator): - return hash((self.value.op, tuple(ValueKey(o) for o in self.value.operands))) + self._hash = hash((self.value.operator, + tuple(ValueKey(o) for o in self.value.operands))) elif isinstance(self.value, Slice): - return hash((ValueKey(self.value.value), self.value.start, self.value.end)) + self._hash = hash((ValueKey(self.value.value), self.value.start, self.value.stop)) elif isinstance(self.value, Part): - return hash((ValueKey(self.value.value), ValueKey(self.value.offset), - self.value.width)) + self._hash = hash((ValueKey(self.value.value), ValueKey(self.value.offset), + self.value.width, self.value.stride)) elif isinstance(self.value, Cat): - return hash(tuple(ValueKey(o) for o in self.value.parts)) + self._hash = hash(tuple(ValueKey(o) for o in self.value.parts)) elif isinstance(self.value, ArrayProxy): - return hash((ValueKey(self.value.index), - tuple(ValueKey(e) for e in self.value._iter_as_values()))) + self._hash = hash((ValueKey(self.value.index), + tuple(ValueKey(e) for e in self.value._iter_as_values()))) elif isinstance(self.value, Sample): - return hash((ValueKey(self.value.value), self.value.clocks, self.value.domain)) + self._hash = hash((ValueKey(self.value.value), self.value.clocks, self.value.domain)) + elif isinstance(self.value, Initial): + self._hash = 0 else: # :nocov: - raise TypeError("Object '{!r}' cannot be used as a key in value collections" + raise TypeError("Object {!r} cannot be used as a key in value collections" .format(self.value)) + def __hash__(self): + return self._hash + def __eq__(self, other): if type(other) is not ValueKey: return False @@ -1245,18 +1678,19 @@ class ValueKey: elif isinstance(self.value, (ClockSignal, ResetSignal)): return self.value.domain == other.value.domain elif isinstance(self.value, Operator): - return (self.value.op == other.value.op and + return (self.value.operator == other.value.operator and len(self.value.operands) == len(other.value.operands) and all(ValueKey(a) == ValueKey(b) for a, b in zip(self.value.operands, other.value.operands))) elif isinstance(self.value, Slice): return (ValueKey(self.value.value) == ValueKey(other.value.value) and self.value.start == other.value.start and - self.value.end == other.value.end) + self.value.stop == other.value.stop) elif isinstance(self.value, Part): return (ValueKey(self.value.value) == ValueKey(other.value.value) and ValueKey(self.value.offset) == ValueKey(other.value.offset) and - self.value.width == other.value.width) + self.value.width == other.value.width and + self.value.stride == other.value.stride) elif isinstance(self.value, Cat): return all(ValueKey(a) == ValueKey(b) for a, b in zip(self.value.parts, other.value.parts)) @@ -1270,8 +1704,10 @@ class ValueKey: return (ValueKey(self.value.value) == ValueKey(other.value.value) and self.value.clocks == other.value.clocks and self.value.domain == self.value.domain) + elif isinstance(self.value, Initial): + return True else: # :nocov: - raise TypeError("Object '{!r}' cannot be used as a key in value collections" + raise TypeError("Object {!r} cannot be used as a key in value collections" .format(self.value)) def __lt__(self, other): @@ -1289,7 +1725,7 @@ class ValueKey: self.value.start < other.value.start and self.value.end < other.value.end) else: # :nocov: - raise TypeError("Object '{!r}' cannot be used as a key in value collections") + raise TypeError("Object {!r} cannot be used as a key in value collections") def __repr__(self): return "<{}.ValueKey {!r}>".format(__name__, self.value) @@ -1307,15 +1743,15 @@ class ValueSet(_MappedKeySet): class SignalKey: def __init__(self, signal): - if type(signal) is Signal: + self.signal = signal + if isinstance(signal, Signal): self._intern = (0, signal.duid) elif type(signal) is ClockSignal: self._intern = (1, signal.domain) elif type(signal) is ResetSignal: self._intern = (2, signal.domain) else: - raise TypeError("Object '{!r}' is not an nMigen signal".format(signal)) - self.signal = signal + raise TypeError("Object {!r} is not an nMigen signal".format(signal)) def __hash__(self): return hash(self._intern) @@ -1327,7 +1763,7 @@ class SignalKey: def __lt__(self, other): if type(other) is not SignalKey: - raise TypeError("Object '{!r}' cannot be compared to a SignalKey".format(signal)) + raise TypeError("Object {!r} cannot be compared to a SignalKey".format(signal)) return self._intern < other._intern def __repr__(self):