hdl.ast: implement ValueCastable.
[nmigen.git] / nmigen / hdl / ast.py
index 622c0fd04659886e98531d7835453f74f817cad9..0b86e7153da0cac1dd5d62c83cca15caaf7357e2 100644 (file)
 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.<domain> +=``.
     """
-    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):