Repository: amaranth-lang/amaranth
Pull request #1113 (2024-02-13 02:16:59)

Add reference documentation for `Value`, `ValueCastable`, `ValueLike`
Except `Value.matches`, `Value.__getitem__`, and `Value.implies`.


Patch:
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 4340df9..6355654 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -36,7 +36,7 @@ class DUID:
 
 
 class Shape:
-    """Bit width and signedness of a value.
+    """Bit width and signedness of a :class:`Value`.
 
     A :class:`Shape` can be obtained by:
 
@@ -326,7 +326,7 @@ class _ShapeLikeMeta(type):
 
 @final
 class ShapeLike(metaclass=_ShapeLikeMeta):
-    """An abstract class representing all objects that can be cast to a :class:`Shape`.
+    """Abstract class representing all objects that can be cast to a :class:`Shape`.
 
     :pc:`issubclass(cls, ShapeLike)` returns :pc:`True` for:
 
@@ -372,13 +372,66 @@ def _overridable_by_reflected(method_name):
 
 
 class Value(metaclass=ABCMeta):
+    """Abstract representation of a bit pattern computed in a circuit.
+
+    The Amaranth language gives Python code the ability to create a circuit netlist by manipulating
+    objects representing the computations within that circuit. The :class:`Value` class represents
+    the bit pattern of a constant, or of a circuit input or output, or within a storage element; or
+    the result of an arithmetic, logical, or bit container operation.
+
+    Operations on this class interpret this bit pattern either as an integer, which can be signed
+    or unsigned depending on the value's :meth:`shape`, or as a bit container. In either case,
+    the semantics of operations that implement Python's syntax, like :pc:`+` (also known as
+    :meth:`__add__`), are identical to the corresponding operation on a Python :class:`int` (or on
+    a Python sequence container). The bitwise inversion :pc:`~` (also known as :meth:`__invert__`)
+    is the sole exception to this rule.
+
+    Data that is not conveniently representable by a single integer or a bit container can be
+    represented by wrapping a :class:`Value` in a :class:`ValueCastable` subclass that provides
+    domain-specific operations. It is possible to extend Amaranth in third-party code using
+    value-castable objects, and the Amaranth standard library provides several built-in ones:
+
+    * :mod:`amaranth.lib.enum` classes are a drop-in replacement for the standard Python
+      :class:`enum` classes that can be defined with an Amaranth shape;
+    * :mod:`amaranth.lib.data` classes allow defining complex data structures such as structures
+      and unions.
+
+    Operations on :class:`Value` instances return another :class:`Value` instance. Unless the exact
+    type and value of the result is explicitly specified below, it should be considered opaque, and
+    may change without notice between Amaranth releases as long as the semantics remains the same.
+
+    .. note::
+
+        In each of the descriptions below, you will see a line similar to:
+
+        **Return type:** :class:`Value`, :pc:`unsigned(1)`, :ref:`assignable <lang-assignable>`
+
+        The first part (:class:`Value`) indicates that the returned object's type is a subclass
+        of :class:`Value`. The second part (:pc:`unsigned(1)`) describes the shape of that value.
+        The third part, if present, indicates that the value is assignable if :pc:`self` is
+        assignable.
+    """
+
     @staticmethod
     def cast(obj):
-        """Converts ``obj`` to an Amaranth value.
+        """Cast :pc:`obj` to an Amaranth value.
+
+        Many :ref:`value-like <lang-valuelike>` objects can be cast to a 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.
-        :class:`ValueCastable` objects are recursively cast to an Amaranth value.
+        * a :class:`Value` instance, where the result is itself;
+        * a :class:`bool` or :class:`int` instance, where the result is :pc:`Const(obj)`;
+        * an :class:`enum.IntEnum` instance, or a :class:`enum.Enum` instance whose members are
+          all integers, where the result is a :class:`Const(obj, enum_shape)` where :pc:`enum_shape`
+          is a shape that can represent every member of the enumeration;
+        * a :class:`ValueCastable` instance, where the result is obtained by repeatedly calling
+          :meth:`obj.as_value() <ValueCastable.as_value>`.
+
+        Raises
+        ------
+        TypeError
+            If :pc:`obj` cannot be converted to a :class:`Value`.
+        RecursionError
+            If :pc:`obj` is a :class:`ValueCastable` object that casts to itself.
         """
         while True:
             if isinstance(obj, Value):
@@ -399,222 +452,623 @@ class Value(metaclass=ABCMeta):
         super().__init__()
         self.src_loc = tracer.get_src_loc(1 + src_loc_at)
 
+    @abstractmethod
+    def shape(self):
+        """Shape of :pc:`self`.
+
+        Returns
+        -------
+        :ref:`shape-like object <lang-shapelike>`
+
+        ..
+            TODO: while this is documented as returning a shape-like object, in practice we
+            guarantee that this is a concrete Shape. it's unclear whether we will ever want to
+            return a shape-catable object here, but there is not much harm in stating a relaxed
+            contract, as it can always be tightened later, but not vice-versa
+        """
+        pass # :nocov:
+
+    def as_unsigned(self):
+        """Reinterpretation as an unsigned value.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self))`, :ref:`assignable <lang-assignable>`
+        """
+        return Operator("u", [self])
+
+    def as_signed(self):
+        """Reinterpretation as a signed value.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`signed(len(self))`, :ref:`assignable <lang-assignable>`
+        """
+        return Operator("s", [self])
+
     def __bool__(self):
+        """Forbidden conversion to boolean.
+
+        Python uses this operator for its built-in semantics, e.g. :pc:`if`, and requires it to
+        return a :class:`bool`. Since this is not possible for Amaranth values, this operator
+        always raises an exception.
+
+        Raises
+        ------
+        :exc:`TypeError`
+            Always.
+        """
         raise TypeError("Attempted to convert Amaranth value to Python boolean")
 
+    def bool(self):
+        """Conversion to boolean.
+
+        Performs the same operation as :meth:`any`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
+        return Operator("b", [self])
+
     def __pos__(self):
+        """Unary position, :pc:`+self`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`self.shape()`
+            :pc:`self`
+        """
         return self
 
-    def __invert__(self):
-        return Operator("~", [self])
     def __neg__(self):
+        """Unary negation, :pc:`-self`.
+
+        ..
+            >>> C(-1).value, C(-1).shape()
+            -1, signed(1)
+            >>> C(-(-1), signed(1)).value # overflows
+            -1
+
+        Returns
+        -------
+        :class:`Value`, :pc:`signed(len(self) + 1)`
+        """
         return Operator("-", [self])
 
     @_overridable_by_reflected("__radd__")
     def __add__(self, other):
+        """Addition, :pc:`self + other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(max(self.width(), other.width()) + 1)`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(max(self.width() + 1, other.width()) + 1)`
+            If :pc:`self` is unsigned and :pc:`other` is signed.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width() + 1) + 1)`
+            If :pc:`self` is signed and :pc:`other` is unsigned.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width()) + 1)`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        """
         return Operator("+", [self, other], src_loc_at=1)
+
     def __radd__(self, other):
+        """Addition, :pc:`other + self` (reflected).
+
+        Like :meth:`__add__`, with operands swapped.
+        """
         return Operator("+", [other, self])
+
     @_overridable_by_reflected("__rsub__")
     def __sub__(self, other):
+        """Subtraction, :pc:`self - other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`signed(max(self.width(), other.width()) + 1)`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(max(self.width() + 1, other.width()) + 1)`
+            If :pc:`self` is unsigned and :pc:`other` is signed.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width() + 1) + 1)`
+            If :pc:`self` is signed and :pc:`other` is unsigned.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width()) + 1)`
+            If both :pc:`self` and :pc:`other` are unsigned.
+
+        Returns
+        -------
+        :class:`Value`
+        """
         return Operator("-", [self, other], src_loc_at=1)
+
     def __rsub__(self, other):
+        """Subtraction, :pc:`other - self` (reflected).
+
+        Like :meth:`__sub__`, with operands swapped.
+        """
         return Operator("-", [other, self])
 
     @_overridable_by_reflected("__rmul__")
     def __mul__(self, other):
+        """Multiplication, :pc:`self * other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self) + len(other))`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(len(self) + len(other))`
+            If either :pc:`self` or :pc:`other` are signed.
+        """
         return Operator("*", [self, other], src_loc_at=1)
+
     def __rmul__(self, other):
+        """Multiplication, :pc:`other * self` (reflected).
+
+        Like :meth:`__mul__`, with operands swapped.
+        """
         return Operator("*", [other, self])
 
-    @_overridable_by_reflected("__rmod__")
-    def __mod__(self, other):
-        return Operator("%", [self, other], src_loc_at=1)
-    def __rmod__(self, other):
-        return Operator("%", [other, self])
     @_overridable_by_reflected("__rfloordiv__")
     def __floordiv__(self, other):
+        """Flooring division, :pc:`self // other`.
+
+        If :pc:`other` is zero, the result of this operation is zero.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self))`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(len(self) + 1)`
+            If :pc:`self` is unsigned and :pc:`other` is signed.
+        :class:`Value`, :pc:`signed(len(self))`
+            If :pc:`self` is signed and :pc:`other` is unsigned.
+        :class:`Value`, :pc:`signed(len(self) + 1)`
+            If both :pc:`self` and :pc:`other` are signed.
+        """
         return Operator("//", [self, other], src_loc_at=1)
+
     def __rfloordiv__(self, other):
+        """Flooring division, :pc:`other // self` (reflected).
+
+        If :pc:`self` is zero, the result of this operation is zero.
+
+        Like :meth:`__floordiv__`, with operands swapped.
+        """
         return Operator("//", [other, self])
 
-    def __check_shamt(self):
-        if self.shape().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")
-    @_overridable_by_reflected("__rlshift__")
-    def __lshift__(self, other):
-        other = Value.cast(other)
-        other.__check_shamt()
-        return Operator("<<", [self, other], src_loc_at=1)
-    def __rlshift__(self, other):
-        self.__check_shamt()
-        return Operator("<<", [other, self])
-    @_overridable_by_reflected("__rrshift__")
-    def __rshift__(self, other):
-        other = Value.cast(other)
-        other.__check_shamt()
-        return Operator(">>", [self, other], src_loc_at=1)
-    def __rrshift__(self, other):
-        self.__check_shamt()
-        return Operator(">>", [other, self])
+    @_overridable_by_reflected("__rmod__")
+    def __mod__(self, other):
+        """Flooring modulo or remainder, :pc:`self % other`.
 
-    @_overridable_by_reflected("__rand__")
-    def __and__(self, other):
-        return Operator("&", [self, other], src_loc_at=1)
-    def __rand__(self, other):
-        return Operator("&", [other, self])
-    @_overridable_by_reflected("__rxor__")
-    def __xor__(self, other):
-        return Operator("^", [self, other], src_loc_at=1)
-    def __rxor__(self, other):
-        return Operator("^", [other, self])
-    @_overridable_by_reflected("__ror__")
-    def __or__(self, other):
-        return Operator("|", [self, other], src_loc_at=1)
-    def __ror__(self, other):
-        return Operator("|", [other, self])
+        If :pc:`other` is zero, the result of this operation is zero.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`other.shape()`
+        """
+        return Operator("%", [self, other], src_loc_at=1)
+
+    def __rmod__(self, other):
+        """Flooring modulo or remainder, :pc:`other % self` (reflected).
+
+        Like :meth:`__mod__`, with operands swapped.
+        """
+        return Operator("%", [other, self])
 
     @_overridable_by_reflected("__eq__")
     def __eq__(self, other):
+        """Equality comparison, :pc:`self == other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
         return Operator("==", [self, other], src_loc_at=1)
+
     @_overridable_by_reflected("__ne__")
     def __ne__(self, other):
+        """Inequality comparison, :pc:`self != other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
         return Operator("!=", [self, other], src_loc_at=1)
+
     @_overridable_by_reflected("__gt__")
     def __lt__(self, other):
+        """Less than comparison, :pc:`self < other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
         return Operator("<", [self, other], src_loc_at=1)
+
     @_overridable_by_reflected("__ge__")
     def __le__(self, other):
+        """Less than or equals comparison, :pc:`self <= other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
         return Operator("<=", [self, other], src_loc_at=1)
+
     @_overridable_by_reflected("__lt__")
     def __gt__(self, other):
+        """Greater than comparison, :pc:`self > other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
         return Operator(">", [self, other], src_loc_at=1)
+
     @_overridable_by_reflected("__le__")
     def __ge__(self, other):
+        """Greater than or equals comparison, :pc:`self >= other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
         return Operator(">=", [self, other], src_loc_at=1)
 
     def __abs__(self):
+        """Absolute value, :pc:`abs(self)`.
+
+        ..
+            >>> abs(C(-1)).shape()
+            unsigned(1)
+            >>> C(1).shape()
+            unsigned(1)
+
+        Return
+        ------
+        :class:`Value`, :pc:`unsigned(len(self))`
+        """
         if self.shape().signed:
             return Mux(self >= 0, self, -self)[:len(self)]
         else:
             return self
 
-    def __len__(self):
-        return self.shape().width
+    def __invert__(self):
+        """Bitwise NOT, :pc:`~self`.
 
-    def __getitem__(self, key):
-        n = len(self)
-        if isinstance(key, int):
-            if key not in range(-n, 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, src_loc_at=1)
-        elif isinstance(key, slice):
-            if isinstance(key.start, Value) or isinstance(key.stop, Value):
-                raise TypeError(f"Cannot slice value with a value; use Value.bit_select() or Value.word_select() instead")
-            start, stop, step = key.indices(n)
-            if step != 1:
-                return Cat(self[i] for i in range(start, stop, step))
-            return Slice(self, start, stop, src_loc_at=1)
-        elif isinstance(key, Value):
-            raise TypeError(f"Cannot index value with a value; use Value.bit_select() instead")
-        else:
-            raise TypeError(f"Cannot index value with {key!r}")
+        The shape of the result is the same as the shape of :pc:`self`, even for unsigned values.
 
-    def __contains__(self, other):
-        raise TypeError("Cannot use 'in' with an Amaranth value")
+        .. important::
 
-    def as_unsigned(self):
-        """Conversion to unsigned.
+            In Python, :pc:`~0` equals :pc:`-1`. In Amaranth, :pc:`~C(0)` equals :pc:`C(1)`.
+            This is the only case where an Amaranth operator deviates from the Python operator
+            with the same name.
+
+            This deviation is necessary because Python does not allow overriding the logical
+            :pc:`and`, :pc:`or`, and :pc:`not` operators. Amaranth uses :pc:`&`, :pc:`|`, and
+            :pc:`~` instead; if it wasn't the case that :pc:`~C(0) == C(1)`, that would have
+            been impossible.
 
         Returns
         -------
-        Value, out
-            This ``Value`` reinterpreted as a unsigned integer.
+        :class:`Value`, :pc:`self.shape()`
         """
-        return Operator("u", [self])
+        return Operator("~", [self])
 
-    def as_signed(self):
-        """Conversion to signed.
+    @_overridable_by_reflected("__rand__")
+    def __and__(self, other):
+        """Bitwise AND, :pc:`self & other`.
 
         Returns
         -------
-        Value, out
-            This ``Value`` reinterpreted as a signed integer.
+        :class:`Value`, :pc:`unsigned(max(self.width(), other.width()))`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(max(self.width() + 1, other.width()))`
+            If :pc:`self` is unsigned and :pc:`other` is signed.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width() + 1))`
+            If :pc:`self` is signed and :pc:`other` is unsigned.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width()))`
+            If both :pc:`self` and :pc:`other` are unsigned.
         """
-        return Operator("s", [self])
+        return Operator("&", [self, other], src_loc_at=1)
 
-    def bool(self):
-        """Conversion to boolean.
+    def __rand__(self, other):
+        """Bitwise AND, :pc:`other & self`.
+
+        Like :meth:`__and__`, with operands swapped.
+        """
+        return Operator("&", [other, self])
+
+    def all(self):
+        """Reduction AND; are all bits :pc:`1`?
 
         Returns
         -------
-        Value, out
-            ``1`` if any bits are set, ``0`` otherwise.
+        :class:`Value`, :pc:`unsigned(1)`
         """
-        return Operator("b", [self])
+        return Operator("r&", [self])
+
+    @_overridable_by_reflected("__ror__")
+    def __or__(self, other):
+        """Bitwise OR, :pc:`self | other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(max(self.width(), other.width()))`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(max(self.width() + 1, other.width()))`
+            If :pc:`self` is unsigned and :pc:`other` is signed.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width() + 1))`
+            If :pc:`self` is signed and :pc:`other` is unsigned.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width()))`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        """
+        return Operator("|", [self, other], src_loc_at=1)
+
+    def __ror__(self, other):
+        """Bitwise OR, :pc:`other | self`.
+
+        Like :meth:`__or__`, with operands swapped.
+        """
+        return Operator("|", [other, self])
 
     def any(self):
-        """Check if any bits are ``1``.
+        """Reduction OR; is any bit :pc:`1`?
 
         Returns
         -------
-        Value, out
-            ``1`` if any bits are set, ``0`` otherwise.
+        :class:`Value`, :pc:`unsigned(1)`
         """
         return Operator("r|", [self])
 
-    def all(self):
-        """Check if all bits are ``1``.
+    @_overridable_by_reflected("__rxor__")
+    def __xor__(self, other):
+        """Bitwise XOR, :pc:`self ^ other`.
 
         Returns
         -------
-        Value, out
-            ``1`` if all bits are set, ``0`` otherwise.
+        :class:`Value`, :pc:`unsigned(max(self.width(), other.width()))`
+            If both :pc:`self` and :pc:`other` are unsigned.
+        :class:`Value`, :pc:`signed(max(self.width() + 1, other.width()))`
+            If :pc:`self` is unsigned and :pc:`other` is signed.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width() + 1))`
+            If :pc:`self` is signed and :pc:`other` is unsigned.
+        :class:`Value`, :pc:`signed(max(self.width(), other.width()))`
+            If both :pc:`self` and :pc:`other` are unsigned.
         """
-        return Operator("r&", [self])
+        return Operator("^", [self, other], src_loc_at=1)
+
+    def __rxor__(self, other):
+        """Bitwise XOR, :pc:`other ^ self`.
+
+        Like :meth:`__xor__`, with operands swapped.
+        """
+        return Operator("^", [other, self])
+
+    def xor(self):
+        """Reduction XOR; is an odd amount of bits :pc:`1`?
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(1)`
+        """
+        return Operator("r^", [self])
+
+    def implies(self, conclusion):
+        # TODO: should we document or just deprecate this?
+        return ~self | conclusion
+
+    def __check_shamt(self):
+        if self.shape().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")
+
+    @_overridable_by_reflected("__rlshift__")
+    def __lshift__(self, other):
+        """Left shift by variable amount, :pc:`self << other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self) + 2 ** len(other) - 1)`
+            If :pc:`self` is unsigned.
+        :class:`Value`, :pc:`signed(len(self) + 2 ** len(other) - 1)`
+            If :pc:`self` is signed.
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If :pc:`other` is signed.
+        """
+        other = Value.cast(other)
+        other.__check_shamt()
+        return Operator("<<", [self, other], src_loc_at=1)
+
+    def __rlshift__(self, other):
+        """Left shift by variable amount, :pc:`other << self`.
+
+        Like :meth:`__lshift__`, with operands swapped.
+        """
+        self.__check_shamt()
+        return Operator("<<", [other, self])
+
+    def shift_left(self, amount):
+        """Left shift by constant amount.
+
+        If :pc:`amount < 0`, performs the same operation as :pc:`self.shift_right(-amount)`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self) + amount)`
+            If :pc:`self` is unsigned.
+        :class:`Value`, :pc:`signed(len(self) + amount)`
+            If :pc:`self` is signed.
+        """
+        if not isinstance(amount, int):
+            raise TypeError(f"Shift amount must be an integer, not {amount!r}")
+        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 rotate_left(self, amount):
+        """Left rotate by constant amount.
+
+        If :pc:`amount < 0`, performs the same operation as :pc:`self.rotate_right(-amount)`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self))`, :ref:`assignable <lang-assignable>`
+        """
+        if not isinstance(amount, int):
+            raise TypeError(f"Rotate amount must be an integer, not {amount!r}")
+        if len(self) != 0:
+            amount %= len(self)
+        return Cat(self[-amount:], self[:-amount]) # meow :3
+
+    @_overridable_by_reflected("__rrshift__")
+    def __rshift__(self, other):
+        """Right shift by variable amount, :pc:`self >> other`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self))`
+            If :pc:`self` is unsigned.
+        :class:`Value`, :pc:`signed(len(self))`
+            If :pc:`self` is signed.
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If :pc:`other` is signed.
+        """
+        other = Value.cast(other)
+        other.__check_shamt()
+        return Operator(">>", [self, other], src_loc_at=1)
+
+    def __rrshift__(self, other):
+        """Right shift by variable amount, :pc:`other >> self`.
+
+        Like :meth:`__rshift__`, with operands swapped.
+        """
+        self.__check_shamt()
+        return Operator(">>", [other, self])
+
+    def shift_right(self, amount):
+        """Right shift by constant amount.
+
+        If :pc:`amount < 0`, performs the same operation as :pc:`self.left_right(-amount)`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(max(len(self) - amount, 0))`
+            If :pc:`self` is unsigned.
+        :class:`Value`, :pc:`signed(max(len(self) - amount, 1))`
+            If :pc:`self` is signed.
+        """
+        if not isinstance(amount, int):
+            raise TypeError(f"Shift amount must be an integer, not {amount!r}")
+        if amount < 0:
+            return self.shift_left(-amount)
+        if self.shape().signed:
+            return self[amount:].as_signed()
+        else:
+            return self[amount:] # unsigned
+
+    def rotate_right(self, amount):
+        """Right rotate by constant amount.
+
+        If :pc:`amount < 0`, performs the same operation as :pc:`self.rotate_right(-amount)`.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self))`, :ref:`assignable <lang-assignable>`
+        """
+        if not isinstance(amount, int):
+            raise TypeError(f"Rotate amount must be an integer, not {amount!r}")
+        if len(self) != 0:
+            amount %= len(self)
+        return Cat(self[amount:], self[:amount])
+
+    def __len__(self):
+        """Bit width of :pc:`self`.
+
+        Returns
+        -------
+        :class:`int`
+            :pc:`self.shape().width`
+        """
+        return self.shape().width
+
+    def __getitem__(self, key):
+        """Bit slicing.
+
+        .. todo::
+
+            Describe this operation.
+        """
+        n = len(self)
+        if isinstance(key, int):
+            if key not in range(-n, 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, src_loc_at=1)
+        elif isinstance(key, slice):
+            if isinstance(key.start, Value) or isinstance(key.stop, Value):
+                raise TypeError(f"Cannot slice value with a value; use Value.bit_select() or "
+                                f"Value.word_select() instead")
+            start, stop, step = key.indices(n)
+            if step != 1:
+                return Cat(self[i] for i in range(start, stop, step))
+            return Slice(self, start, stop, src_loc_at=1)
+        elif isinstance(key, Value):
+            raise TypeError(f"Cannot index value with a value; use Value.bit_select() instead")
+        else:
+            raise TypeError(f"Cannot index value with {key!r}")
 
-    def xor(self):
-        """Compute pairwise exclusive-or of every bit.
+    def __contains__(self, other):
+        """Forbidden membership test operator.
 
-        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])
+        Python requires this operator to return a :class:`bool`. Since this is not possible
+        for Amaranth values, this operator always raises an exception.
 
-    def implies(premise, conclusion):
-        """Implication.
+        To check membership in a set of constant integer values, use :meth:`matches` instead.
 
-        Returns
-        -------
-        Value, out
-            ``0`` if ``premise`` is true and ``conclusion`` is not, ``1`` otherwise.
+        Raises
+        ------
+        :exc:`TypeError`
+            Always.
         """
-        return ~premise | conclusion
+        raise TypeError("Cannot use 'in' with an Amaranth value")
 
     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.
+        Selects a constant width, variable offset part of :pc:`self`, where parts with successive
+        offsets overlap by :pc:`width - 1` bits. When :pc:`offset` is a constant integer, this
+        operation is equivalent to :pc:`self[offset:offset + width]`.
 
         Parameters
         ----------
-        offset : Value, int
-            Index of first selected bit.
-        width : int
-            Number of selected bits.
+        offset: :ref:`value-like <lang-valuelike>`
+            Index of the first selected bit.
+        width: :class:`int`
+            Amount of bits to select.
 
         Returns
         -------
-        Part, out
-            Selected part of the ``Value``
+        :class:`Value`, :pc:`unsigned(width)`, :ref:`assignable <lang-assignable>`
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If :pc:`offset` is signed.
+        :exc:`TypeError`
+            If :pc:`width` is negative.
         """
         offset = Value.cast(offset)
         if type(offset) is Const and isinstance(width, int):
@@ -624,41 +1078,67 @@ class Value(metaclass=ABCMeta):
     def word_select(self, offset, width):
         """Part-select with word granularity.
 
-        Selects a constant width but variable offset part of a ``Value``, such that successive
-        parts do not overlap.
+        Selects a constant width, variable offset part of :pc:`self`, where parts with successive
+        offsets are adjacent but do not overlap. When :pc:`offset` is a constant integer, this
+        operation is equivalent to :pc:`self[offset * width:(offset + 1) * width]`.
 
         Parameters
         ----------
-        offset : Value, int
-            Index of first selected word.
-        width : int
-            Number of selected bits.
+        offset: :ref:`value-like <lang-valuelike>`
+            Index of the first selected word.
+        width: :class:`int`
+            Amount of bits to select.
 
         Returns
         -------
-        Part, out
-            Selected part of the ``Value``
+        :class:`Value`, :pc:`unsigned(width)`, :ref:`assignable <lang-assignable>`
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If :pc:`offset` is signed.
+        :exc:`TypeError`
+            If :pc:`width` is negative.
         """
         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 replicate(self, count):
+        """Replication.
+
+        Equivalent to :pc:`Cat(self for _ in range(count))`, but not assignable.
+
+        ..
+            Technically assignable right now, but we don't want to commit to that.
+
+        Returns
+        -------
+        :class:`Value`, :pc:`unsigned(len(self) * count)`
+
+        Raises
+        ------
+        :exc:`TypeError`
+            If :pc:`count` is negative.
+        """
+        if not isinstance(count, int) or count < 0:
+            raise TypeError("Replication count must be a non-negative integer, not {!r}"
+                            .format(count))
+        return Cat(self for _ in range(count))
+
     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()``.
+        Matches against a set of patterns, recognizing the same grammar as :pc:`with m.Case()`.
 
-        Parameters
-        ----------
-        patterns : int or str
-            Patterns to match against.
+        .. todo::
+
+            Describe the pattern language in detail.
 
         Returns
         -------
-        Value, out
-            ``1`` if any pattern matches the value, ``0`` otherwise.
+        :class:`Value`, :pc:`unsigned(1)`
         """
         matches = []
         # This code should accept exactly the same patterns as `with m.Case(...):`.
@@ -699,150 +1179,217 @@ class Value(metaclass=ABCMeta):
         else:
             return Cat(*matches).any()
 
-    def shift_left(self, amount):
-        """Shift left by constant amount.
+    def eq(self, value, *, src_loc_at=0):
+        """:ref:`Assignment <lang-assigns>`.
 
-        Parameters
-        ----------
-        amount : int
-            Amount to shift by.
+        Once it is placed in a domain, an assignment changes the bit pattern of :pc:`self` to
+        equal :pc:`value`. If the bit width of :pc:`value` is less than that of :pc:`self`,
+        it is zero-extended (for unsigned :pc:`value`\\ s) or sign-extended (for signed
+        :pc:`value`\\ s). If the bit width of :pc:`value` is greater than that of :pc:`self`,
+        it is truncated.
 
         Returns
         -------
-        Value, out
-            If the amount is positive, the input shifted left. Otherwise, the input shifted right.
+        :class:`Statement`
         """
-        if not isinstance(amount, int):
-            raise TypeError(f"Shift amount must be an integer, not {amount!r}")
-        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
+        return Assign(self, value, src_loc_at=src_loc_at + 1)
+
+    #: Forbidden hashing.
+    #:
+    #: Python objects are :term:`python:hashable` if they provide a :pc:`__hash__` method
+    #: that returns an :class:`int` and an :pc:`__eq__` method that returns a :class:`bool`.
+    #: Amaranth values define :meth:`__eq__` to return a :class:`Value`, which precludes them
+    #: from being hashable.
+    #:
+    #: To use a :class:`Value` as a key in a :class:`dict`, use the following pattern:
+    #:
+    #: .. testcode::
+    #:
+    #:      value = Signal()
+    #:      assoc = {}
+    #:      assoc[id(value)] = value, "a signal"
+    #:      _, info = assoc[id(value)]
+    #:      assert info == "a signal"
+    __hash__ = None # type: ignore
 
-    def shift_right(self, amount):
-        """Shift right by constant amount.
+    def _lhs_signals(self):
+        raise TypeError(f"Value {self!r} cannot be used in assignments")
 
-        Parameters
-        ----------
-        amount : int
-            Amount to shift by.
+    @abstractmethod
+    def _rhs_signals(self):
+        raise NotImplementedError # :nocov:
 
-        Returns
-        -------
-        Value, out
-            If the amount is positive, the input shifted right. Otherwise, the input shifted left.
-        """
-        if not isinstance(amount, int):
-            raise TypeError(f"Shift amount must be an integer, not {amount!r}")
-        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.
+class ValueCastable:
+    """Interface class for objects that can be cast to a :class:`Value`.
 
-        Parameters
-        ----------
-        amount : int
-            Amount to rotate by.
+    Computations in the Amaranth language are described by combining :ref:`value-like objects
+    <lang-valuelike>`. Inheriting a class from :class:`ValueCastable` and implementing
+    all of the methods described below adds instances of that class to the list of
+    value-like objects recognized by the :meth:`Value.cast` method. This is a part of the mechanism
+    for seamlessly extending the Amaranth language in third-party code.
 
-        Returns
-        -------
-        Value, out
-            If the amount is positive, the input rotated left. Otherwise, the input rotated right.
-        """
-        if not isinstance(amount, int):
-            raise TypeError(f"Rotate amount must be an integer, not {amount!r}")
-        if len(self) != 0:
-            amount %= len(self)
-        return Cat(self[-amount:], self[:-amount]) # meow :3
+    .. note::
 
-    def rotate_right(self, amount):
-        """Rotate right by constant amount.
+        All methods and operators defined by the :class:`Value` class will implicitly cast
+        a :class:`ValueCastable` object to a :class:`Value`, with the exception of arithmetic
+        operators, which will prefer calling a reflected arithmetic operation on
+        the :class:`ValueCastable` argument if it defines one.
 
-        Parameters
-        ----------
-        amount : int
-            Amount to rotate by.
+        For example, if :pc:`value_castable` implements :pc:`__radd__`, then
+        :pc:`C(1) + value_castable` will perform :pc:`value_castable.__radd__(C(1))`, and otherwise
+        it will perform :pc:`C(1).__add__(value_castable.as_value())`.
+    """
 
-        Returns
-        -------
-        Value, out
-            If the amount is positive, the input rotated right. Otherwise, the input rotated right.
-        """
-        if not isinstance(amount, int):
-            raise TypeError(f"Rotate amount must be an integer, not {amount!r}")
-        if len(self) != 0:
-            amount %= len(self)
-        return Cat(self[amount:], self[:amount])
+    def __init__(self, *args, **kwargs):
+        if type(self) is ValueCastable:
+            raise TypeError("Can't instantiate abstract class ValueCastable")
+        super().__init__(*args, **kwargs)
 
-    def replicate(self, count):
-        """Replication.
+    def __init_subclass__(cls, **kwargs):
+        if cls.as_value is ValueCastable.as_value:
+            raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override "
+                            "the 'as_value' method")
+        if cls.shape is ValueCastable.shape:
+            raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override "
+                            "the 'shape' method")
+        if not hasattr(cls.as_value, "_ValueCastable__memoized"):
+            raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must decorate "
+                            "the 'as_value' method with the 'ValueCastable.lowermethod' decorator")
 
-        A ``Value`` is replicated (repeated) several times to be used
-        on the RHS of assignments::
+    # The signatures and definitions of these methods are weird because they are present here for
+    # documentation (and error checking above) purpose only and should not affect control flow.
 
-            len(v.replicate(n)) == len(v) * n
+    def as_value(self, *args, **kwargs):
+        """as_value()
 
-        Parameters
-        ----------
-        count : int
-            Number of replications.
+        Convert :pc:`self` to a :ref:`value-like object <lang-valuelike>`.
+
+        This method is called by the Amaranth language to convert :pc:`self` to a concrete
+        :class:`Value`. It will usually return a :class:`Value` object, but it may also return
+        another value-like object to delegate its functionality.
+
+        This method must be idempotent: when called twice on the same object, the result must be
+        exactly the same.
+
+        This method may also be called by code that is not a part of the Amaranth language.
 
         Returns
         -------
-        Value, out
-            Replicated value.
+        Any other object recognized by :meth:`Value.cast`.
+
+        Raises
+        ------
+        Exception
+            When the conversion cannot be done. This exception must be propagated by callers,
+            either directly or as a cause of another exception.
+
+            It is recommended that, in cases where this method raises an exception,
+            the :meth:`shape` method also raises an exception.
         """
-        if not isinstance(count, int) or count < 0:
-            raise TypeError("Replication count must be a non-negative integer, not {!r}"
-                            .format(count))
-        return Cat(self for _ in range(count))
+        return super().as_value(*args, **kwargs) # :nocov:
 
-    def eq(self, value):
-        """Assignment.
+    def shape(self, *args, **kwargs):
+        """shape()
 
-        Parameters
-        ----------
-        value : Value, in
-            Value to be assigned.
+        Compute the shape of :pc:`self`.
+
+        This method is not called by the Amaranth language itself; whenever it needs to discover
+        the shape of a value-castable object, it calls :class:`self.as_value().shape()`. However,
+        that method must return a :class:`Shape`, and :class:`ValueCastable` subclasses may have
+        a richer representation of their shape provided by an instance of a :class:`ShapeCastable`
+        subclass. This method may return such a representation.
+
+        This method must be idempotent: when called twice on the same object, the result must be
+        exactly the same.
+
+        The following condition must hold:
+
+        .. code::
+
+            Shape.cast(self.shape()) == Value.cast(self).shape()
 
         Returns
         -------
-        Assign
-            Assignment statement that can be used in combinatorial or synchronous context.
+        A :ref:`shape-like <lang-shapelike>` object.
+
+        Raises
+        ------
+        Exception
+            When the conversion cannot be done. This exception must be propagated by callers,
+            either directly or as a cause of another exception.
+
+            It is recommended that, in cases where this method raises an exception,
+            the :meth:`as_value` method also raises an exception.
         """
-        return Assign(self, value, src_loc_at=1)
+        return super().shape(*args, **kwargs) # :nocov:
 
-    @abstractmethod
-    def shape(self):
-        """Bit width and signedness of a value.
+    @staticmethod
+    def lowermethod(func):
+        """Decorator to memoize lowering methods.
 
-        Returns
-        -------
-        Shape
-            See :class:`Shape`.
-
-        Examples
-        --------
-        >>> Signal(8).shape()
-        Shape(width=8, signed=False)
-        >>> Const(0xaa).shape()
-        Shape(width=8, signed=False)
+        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 :pc:`as_value` method of :pc:`ValueCastable`
+        subclasses. This is to ensure that Amaranth's view of representation of all values stays
+        internally consistent.
         """
-        pass # :nocov:
+        @functools.wraps(func)
+        def wrapper_memoized(self, *args, **kwargs):
+            # Use `in self.__dict__` instead of `hasattr` to avoid interfering with custom
+            # `__getattr__` implementations.
+            if not "_ValueCastable__lowered_to" in self.__dict__:
+                self.__lowered_to = func(self, *args, **kwargs)
+            return self.__lowered_to
+        wrapper_memoized.__memoized = True
+        return wrapper_memoized
 
-    def _lhs_signals(self):
-        raise TypeError(f"Value {self!r} cannot be used in assignments")
 
-    @abstractmethod
-    def _rhs_signals(self):
-        raise NotImplementedError # :nocov:
+class _ValueLikeMeta(type):
+    def __subclasscheck__(cls, subclass):
+        if issubclass(subclass, (Value, ValueCastable, int)) or subclass is ValueLike:
+            return True
+        if issubclass(subclass, Enum):
+            return isinstance(subclass, ShapeLike)
+        return False
+
+    def __instancecheck__(cls, instance):
+        return issubclass(type(instance), cls)
+
+
+@final
+class ValueLike(metaclass=_ValueLikeMeta):
+    """Abstract class representing all objects that can be cast to a :class:`Value`.
+
+    :pc:`issubclass(cls, ValueLike)` returns :pc:`True` for:
+
+    * :class:`Value`;
+    * :class:`ValueCastable` and its subclasses;
+    * :class:`int` and its subclasses (including :class:`bool`);
+    * :class:`enum.Enum` subclasses where all values are :ref:`value-like <lang-valuelike>`;
+    * :class:`ValueLike` itself.
+
+    :pc:`isinstance(obj, ValueLike)` returns the same value as
+    :pc:`issubclass(type(obj), ValueLike)`.
+
+    This class cannot be instantiated or subclassed. It can only be used for checking types of
+    objects.
+
+    .. note::
+
+        It is possible to define an enumeration with a member that is
+        :ref:`value-like <lang-valuelike>` but not :ref:`constant-castable <lang-constcasting>`,
+        meaning that :pc:`issubclass(BadEnum, ValueLike)` returns :pc:`True`, but
+        :pc:`Value.cast(BadEnum.MEMBER)` raises an exception.
+
+        The :mod:`amaranth.lib.enum` module prevents such enumerations from being defined when
+        the shape is specified explicitly. Using :mod:`amaranth.lib.enum` and specifying the shape
+        ensures that all of your enumeration members are constant-castable and fit in the provided
+        shape.
+    """
+    def __new__(cls, *args, **kwargs):
+        raise TypeError("ValueLike is an abstract class and cannot be constructed")
 
 
 class _ConstMeta(ABCMeta):
@@ -984,7 +1531,7 @@ class Operator(Value):
                 return Shape(b_shape.width, b_shape.signed)
             if self.operator in ("<", "<=", "==", "!=", ">", ">="):
                 return Shape(1, False)
-            if self.operator in ("&", "^", "|"):
+            if self.operator in ("&", "|", "^"):
                 return _bitwise_binary_shape(*op_shapes)
             if self.operator == "<<":
                 assert not b_shape.signed
@@ -996,8 +1543,7 @@ class Operator(Value):
             if self.operator == "m":
                 s_shape, a_shape, b_shape = op_shapes
                 return _bitwise_binary_shape(a_shape, b_shape)
-        raise NotImplementedError("Operator {}/{} not implemented"
-                                  .format(self.operator, len(op_shapes))) # :nocov:
+        raise NotImplementedError # :nocov:
 
     def _lhs_signals(self):
         if self.operator in ("u", "s"):
@@ -1465,8 +2011,8 @@ def AnySeq(shape, *, src_loc_at=0):
 class Array(MutableSequence):
     """Addressable multiplexer.
 
-    An array is similar to a ``list`` that can also be indexed by ``Value``s; indexing by an integer or a slice works the same as for Python lists, but indexing by a ``Value`` results
-    in a proxy.
+    An array is similar to a ``list`` that can also be indexed by ``Value``s; indexing by an integer
+    or a slice works the same as for Python lists, but indexing by a ``Value`` results in a proxy.
 
     The array proxy can be used as an ordinary ``Value``, i.e. participate in calculations and
     assignments, provided that all elements of the array are values. The array proxy also supports
@@ -1605,96 +2151,6 @@ class ArrayProxy(Value):
         return "(proxy (array [{}]) {!r})".format(", ".join(map(repr, self.elems)), self.index)
 
 
-class ValueCastable:
-    """Interface of user-defined objects that can be cast to :class:`Value` s.
-
-    An object deriving from :class:`ValueCastable`` is automatically converted to a :class:`Value`
-    when it is used in a context where a :class:`Value`` is expected. Such objects can implement
-    different or richer semantics than what is supported by the core Amaranth language, yet still
-    be transparently used with it as long as the final underlying representation is a single
-    Amaranth :class:`Value`. These objects also need not commit to a specific representation until
-    they are converted to a concrete Amaranth value.
-
-    Note that it is necessary to ensure that Amaranth's view of representation of all values stays
-    internally consistent. The class deriving from :class:`ValueCastable`` must decorate
-    the :meth:`as_value` method with the :meth:`lowermethod` decorator, which ensures that all
-    calls to :meth:`as_value` return the same :class:`Value` representation. If the class deriving
-    from :class:`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 :meth:`as_value`.
-    """
-
-    def __init__(self, *args, **kwargs):
-        if type(self) is ValueCastable:
-            raise TypeError("Can't instantiate abstract class ValueCastable")
-        super().__init__(*args, **kwargs)
-
-    def __init_subclass__(cls, **kwargs):
-        if not hasattr(cls, "as_value"):
-            raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must override "
-                            "the `as_value` method")
-        if not hasattr(cls, "shape"):
-            raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must override "
-                            "the `shape` method")
-        if not hasattr(cls.as_value, "_ValueCastable__memoized"):
-            raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must decorate "
-                            "the `as_value` method with the `ValueCastable.lowermethod` decorator")
-
-    @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 Amaranth's view of representation of all values stays
-        internally consistent.
-        """
-        @functools.wraps(func)
-        def wrapper_memoized(self, *args, **kwargs):
-            # Use `in self.__dict__` instead of `hasattr` to avoid interfering with custom
-            # `__getattr__` implementations.
-            if not "_ValueCastable__lowered_to" in self.__dict__:
-                self.__lowered_to = func(self, *args, **kwargs)
-            return self.__lowered_to
-        wrapper_memoized.__memoized = True
-        return wrapper_memoized
-
-
-class _ValueLikeMeta(type):
-    """An abstract class representing all objects that can be cast to a :class:`Value`.
-
-    ``issubclass(cls, ValueLike)`` returns ``True`` for:
-
-    - :class:`Value`
-    - :class:`ValueCastable` and its subclasses
-    - ``int`` and its subclasses
-    - :class:`enum.Enum` subclasses where all values are :ref:`value-like <lang-valuelike>`
-    - :class:`ValueLike` itself
-
-    ``isinstance(obj, ValueLike)`` returns the same value as ``issubclass(type(obj), ValueLike)``.
-
-    This class is only usable for the above checks — no instances and no (non-virtual)
-    subclasses can be created.
-    """
-
-    def __subclasscheck__(cls, subclass):
-        if issubclass(subclass, (Value, ValueCastable, int)) or subclass is ValueLike:
-            return True
-        if issubclass(subclass, Enum):
-            return isinstance(subclass, ShapeLike)
-        return False
-
-    def __instancecheck__(cls, instance):
-        return issubclass(type(instance), cls)
-
-
-@final
-class ValueLike(metaclass=_ValueLikeMeta):
-    def __new__(cls, *args, **kwargs):
-        raise TypeError("ValueLike is an abstract class and cannot be constructed")
-
-
 @final
 class Initial(Value):
     """Start indicator, for model checking.
diff --git a/docs/guide.rst b/docs/guide.rst
index ccd611d..5b80d89 100644
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -885,12 +885,12 @@ Assigning to signals
 Similar to :ref:`how Amaranth operators work <lang-abstractexpr>`, an Amaranth assignment is an ordinary Python object used to describe a part of a circuit. An assignment does not have any effect on the signal it changes until it is added to a control domain in a module. Once added, it introduces logic into the circuit generated from that module.
 
 
-.. _lang-assignlhs:
+.. _lang-assignable:
 
-Assignment targets
-------------------
+Assignable values
+-----------------
 
-The target of an assignment can be more complex than a single signal. It is possible to assign to any combination of signals, :ref:`bit slices <lang-seqops>`, :ref:`concatenations <lang-seqops>`, :ref:`part selects <lang-seqops>`, and :ref:`array proxy objects <lang-array>` as long as it includes no other values:
+An assignment can affect a value that is more complex than just a signal. It is possible to assign to any combination of :ref:`signals <lang-signals>`, :ref:`bit slices <lang-seqops>`, :ref:`concatenations <lang-seqops>`, :ref:`part selects <lang-seqops>`, and :ref:`array proxy objects <lang-array>` as long as it includes no other values:
 
 .. doctest::
 
diff --git a/docs/reference.rst b/docs/reference.rst
index dfc5a55..10147b6 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -101,5 +101,16 @@ See also the introduction to :ref:`shapes <lang-shapes>` and :ref:`casting from
 .. autoclass:: Shape
 .. autofunction:: unsigned
 .. autofunction:: signed
-.. autoclass:: ShapeCastable
-.. autoclass:: ShapeLike
+.. autoclass:: ShapeCastable()
+.. autoclass:: ShapeLike()
+
+
+Values
+======
+
+See also the introduction to :ref:`values <lang-values>` and :ref:`casting from value-like objects <lang-valuelike>` in the language guide.
+
+.. autoclass:: Value
+    :special-members: __bool__, __pos__, __neg__, __add__, __radd__, __sub__, __rsub__, __mul__, __rmul__, __mod__, __rmod__, __floordiv__, __rfloordiv__, __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __abs__, __invert__, __and__, __rand__, __or__, __ror__, __xor__, __rxor__, __lshift__, __rlshift__, __rshift__, __rrshift__, __len__, __getitem__, __contains__, __hash__
+.. autoclass:: ValueCastable()
+.. autoclass:: ValueLike()
\ No newline at end of file


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 14e1f8a..83bbb77 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -1350,8 +1350,8 @@ class MockValueCastableCustomGetattr(ValueCastable):
 class ValueCastableTestCase(FHDLTestCase):
     def test_not_decorated(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockValueCastableNotDecorated' deriving from `ValueCastable` must "
-                r"decorate the `as_value` method with the `ValueCastable.lowermethod` decorator$"):
+                r"^Class 'MockValueCastableNotDecorated' deriving from 'ValueCastable' must "
+                r"decorate the 'as_value' method with the 'ValueCastable.lowermethod' decorator$"):
             class MockValueCastableNotDecorated(ValueCastable):
                 def __init__(self):
                     pass
@@ -1364,15 +1364,15 @@ class ValueCastableTestCase(FHDLTestCase):
 
     def test_no_override(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockValueCastableNoOverrideAsValue' deriving from `ValueCastable` must "
-                r"override the `as_value` method$"):
+                r"^Class 'MockValueCastableNoOverrideAsValue' deriving from 'ValueCastable' must "
+                r"override the 'as_value' method$"):
             class MockValueCastableNoOverrideAsValue(ValueCastable):
                 def __init__(self):
                     pass
 
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockValueCastableNoOverrideShapec' deriving from `ValueCastable` must "
-                r"override the `shape` method$"):
+                r"^Class 'MockValueCastableNoOverrideShapec' deriving from 'ValueCastable' must "
+                r"override the 'shape' method$"):
             class MockValueCastableNoOverrideShapec(ValueCastable):
                 def __init__(self):
                     pass