Repository: amaranth-lang/amaranth
Pull request #1063 (2024-01-30 22:05:53)

Implement RFC 42: `Const` from shape-castable.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1063?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`5dd1223`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/5dd1223cf8b2fc1accbc4d0e53dc702cce4a19af?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 86.54% compared to head [(`d5aab78`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1063?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 86.55%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1063   +/-   ##
=======================================
  Coverage   86.54%   86.55%           
=======================================
  Files          41       41           
  Lines        7157     7162    +5     
  Branches     1711     1712    +1     
=======================================
+ Hits         6194     6199    +5     
- Misses        796      797    +1     
+ Partials      167      166    -1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1063?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).


Patch:
diff --git a/amaranth/__init__.py b/amaranth/__init__.py
index 95a6cce..4094b99 100644
--- a/amaranth/__init__.py
+++ b/amaranth/__init__.py
@@ -12,10 +12,10 @@ del importlib
 
 from .hdl import *
 
-
+# must be kept in sync with docs/reference.rst!
 __all__ = [
     "Shape", "unsigned", "signed",
-    "Value", "Const", "C", "Mux", "Cat", "Repl", "Array", "Signal", "ClockSignal", "ResetSignal",
+    "Value", "Const", "C", "Mux", "Cat", "Array", "Signal", "ClockSignal", "ResetSignal",
     "Module",
     "ClockDomain",
     "Elaboratable", "Fragment", "Instance",
diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py
index 3c5ffcf..23f6b6c 100644
--- a/amaranth/back/rtlil.py
+++ b/amaranth/back/rtlil.py
@@ -215,9 +215,6 @@ class _ProcessBuilder(_AttrBuilder, _BufferedBuilder):
     def case(self):
         return _CaseBuilder(self, indent=2)
 
-    def sync(self, kind, cond=None):
-        return _SyncBuilder(self, kind, cond)
-
 
 class _CaseBuilder(_ProxiedBuilder):
     def __init__(self, rtlil, indent):
@@ -264,26 +261,6 @@ class _SwitchBuilder(_AttrBuilder, _ProxiedBuilder):
         return _CaseBuilder(self.rtlil, self.indent + 2)
 
 
-class _SyncBuilder(_ProxiedBuilder):
-    def __init__(self, rtlil, kind, cond):
-        self.rtlil = rtlil
-        self.kind  = kind
-        self.cond  = cond
-
-    def __enter__(self):
-        if self.cond is None:
-            self._append("    sync {}\n", self.kind)
-        else:
-            self._append("    sync {} {}\n", self.kind, self.cond)
-        return self
-
-    def __exit__(self, *args):
-        pass
-
-    def update(self, lhs, rhs):
-        self._append("      update {} {}\n", lhs, rhs)
-
-
 def _src(src_loc):
     if src_loc is None:
         return None
@@ -465,27 +442,13 @@ class _RHSValueCompiler(_ValueCompiler):
     def on_Const(self, value):
         return _const(value)
 
-    def on_AnyConst(self, value):
+    def on_AnyValue(self, value):
         if value in self.s.anys:
             return self.s.anys[value]
 
         res_shape = value.shape()
         res = self.s.rtlil.wire(width=res_shape.width, src=_src(value.src_loc))
-        self.s.rtlil.cell("$anyconst", ports={
-            "\\Y": res,
-        }, params={
-            "WIDTH": res_shape.width,
-        }, src=_src(value.src_loc))
-        self.s.anys[value] = res
-        return res
-
-    def on_AnySeq(self, value):
-        if value in self.s.anys:
-            return self.s.anys[value]
-
-        res_shape = value.shape()
-        res = self.s.rtlil.wire(width=res_shape.width, src=_src(value.src_loc))
-        self.s.rtlil.cell("$anyseq", ports={
+        self.s.rtlil.cell("$" + value.kind.value, ports={
             "\\Y": res,
         }, params={
             "WIDTH": res_shape.width,
@@ -642,10 +605,7 @@ class _LHSValueCompiler(_ValueCompiler):
     def on_Const(self, value):
         raise TypeError # :nocov:
 
-    def on_AnyConst(self, value):
-        raise TypeError # :nocov:
-
-    def on_AnySeq(self, value):
+    def on_AnyValue(self, value):
         raise TypeError # :nocov:
 
     def on_Initial(self, value):
@@ -744,21 +704,17 @@ class _StatementCompiler(_xfrm.StatementVisitor):
         else:
             self._case.assign(self.lhs_compiler(stmt.lhs), rhs_sigspec)
 
-    def on_property(self, stmt):
+    def on_Property(self, stmt):
         self(stmt._check.eq(stmt.test))
         self(stmt._en.eq(1))
 
         en_wire = self.rhs_compiler(stmt._en)
         check_wire = self.rhs_compiler(stmt._check)
-        self.state.rtlil.cell("$" + stmt._kind, ports={
+        self.state.rtlil.cell("$" + stmt.kind.value, ports={
             "\\A": check_wire,
             "\\EN": en_wire,
         }, src=_src(stmt.src_loc), name=stmt.name)
 
-    on_Assert = on_property
-    on_Assume = on_property
-    on_Cover  = on_property
-
     def on_Switch(self, stmt):
         self._check_rhs(stmt.test)
 
diff --git a/amaranth/hdl/__init__.py b/amaranth/hdl/__init__.py
index e9fdb85..6ae76f8 100644
--- a/amaranth/hdl/__init__.py
+++ b/amaranth/hdl/__init__.py
@@ -1,6 +1,6 @@
 from ._ast import Shape, unsigned, signed, ShapeCastable, ShapeLike
 from ._ast import Value, ValueCastable, ValueLike
-from ._ast import Const, C, Mux, Cat, Repl, Array, Signal, ClockSignal, ResetSignal
+from ._ast import Const, C, Mux, Cat, Array, Signal, ClockSignal, ResetSignal
 from ._dsl import SyntaxError, SyntaxWarning, Module
 from ._cd import DomainError, ClockDomain
 from ._ir import UnusedElaboratable, Elaboratable, DriverConflict, Fragment, Instance
@@ -13,7 +13,7 @@ __all__ = [
     # _ast
     "Shape", "unsigned", "signed", "ShapeCastable", "ShapeLike",
     "Value", "ValueCastable", "ValueLike",
-    "Const", "C", "Mux", "Cat", "Repl", "Array", "Signal", "ClockSignal", "ResetSignal",
+    "Const", "C", "Mux", "Cat", "Array", "Signal", "ClockSignal", "ResetSignal",
     # _dsl
     "SyntaxError", "SyntaxWarning", "Module",
     # _cd
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 9d4bb27..c2fe98f 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -1,5 +1,4 @@
 from abc import ABCMeta, abstractmethod
-import inspect
 import warnings
 import functools
 import operator
@@ -12,13 +11,12 @@ from ._repr import *
 from .. import tracer
 from ..utils import *
 from .._utils import *
-from .._utils import _ignore_deprecated
 from .._unused import *
 
 
 __all__ = [
     "Shape", "signed", "unsigned", "ShapeCastable", "ShapeLike",
-    "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl",
+    "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat",
     "Array", "ArrayProxy",
     "Signal", "ClockSignal", "ResetSignal",
     "ValueCastable", "ValueLike",
@@ -37,51 +35,23 @@ class DUID:
         DUID.__next_uid += 1
 
 
-class ShapeCastable:
-    """Interface of user-defined objects that can be cast to :class:`Shape` s.
-
-    An object deriving from :class:`ShapeCastable` is automatically converted to a :class:`Shape`
-    when it is used in a context where a :class:`Shape` is expected. Such objects can contain
-    a richer description of the shape than what is supported by the core Amaranth language, yet
-    still be transparently used with it.
-    """
-    def __init_subclass__(cls, **kwargs):
-        if not hasattr(cls, "as_shape"):
-            raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
-                            f"the `as_shape` method")
-        if not (hasattr(cls, "__call__") and inspect.isfunction(cls.__call__)):
-            raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
-                            f"the `__call__` method")
-        if not hasattr(cls, "const"):
-            raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
-                            f"the `const` method")
-
-    def _value_repr(self, value):
-        return (Repr(FormatInt(), value),)
-
-
 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 :class:`Shape` can be obtained by:
 
-    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.
+    * constructing with explicit bit width and signedness;
+    * using the :func:`signed` and :func:`unsigned` aliases if the signedness is known upfront;
+    * casting from a variety of objects using the :meth:`cast` method.
 
     Parameters
     ----------
     width : int
-        The number of bits in the representation, including the sign bit (if any).
+        The number of bits in the representation of a value. This includes the sign bit for signed
+        values. Cannot be zero if the value is signed.
     signed : bool
-        If ``False``, the value is unsigned. If ``True``, the value is signed two's complement.
+        Whether the value is signed. Signed values use the
+        `two's complement <https://en.wikipedia.org/wiki/Two's_complement>`_ representation.
     """
     def __init__(self, width=1, signed=False):
         if not isinstance(width, int):
@@ -117,6 +87,27 @@ class Shape:
 
     @staticmethod
     def cast(obj, *, src_loc_at=0):
+        """Cast :pc:`obj` to a shape.
+
+        Many :ref:`shape-like <lang-shapelike>` objects can be cast to a shape:
+
+        * a :class:`Shape`, where the result is itself;
+        * an :class:`int`, where the result is :func:`unsigned(obj) <unsigned>`;
+        * a :class:`range`, where the result has minimal width required to represent all elements
+          of the range, and is signed if any element of the range is signed;
+        * an :class:`enum.Enum` whose members are all :ref:`constant-castable <lang-constcasting>`
+          or :class:`enum.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;
+        * a :class:`ShapeCastable` object, where the result is obtained by repeatedly calling
+          :meth:`obj.as_shape() <ShapeCastable.as_shape>`.
+
+        Raises
+        ------
+        TypeError
+            If :pc:`obj` cannot be converted to a :class:`Shape`.
+        RecursionError
+            If :pc:`obj` is a :class:`ShapeCastable` object that casts to itself.
+        """
         while True:
             if isinstance(obj, Shape):
                 return obj
@@ -130,6 +121,8 @@ class Shape:
                 signed = obj[0] < 0 or obj[-1] < 0
                 width  = max(bits_for(obj[0], signed),
                              bits_for(obj[-1], signed))
+                if obj[0] == obj[-1] == 0:
+                    width = 0
                 return Shape(width, signed)
             elif isinstance(obj, type) and issubclass(obj, Enum):
                 # For compatibility with third party enumerations, handle them as if they were
@@ -142,6 +135,10 @@ class Shape:
             obj = new_obj
 
     def __repr__(self):
+        """Python code that creates this shape.
+
+        Returns :pc:`f"signed({self.width})"` or :pc:`f"unsigned({self.width})"`.
+        """
         if self.signed:
             return f"signed({self.width})"
         else:
@@ -152,6 +149,158 @@ class Shape:
                 self.width == other.width and self.signed == other.signed)
 
 
+def unsigned(width):
+    """Returns :pc:`Shape(width, signed=False)`."""
+    return Shape(width, signed=False)
+
+
+def signed(width):
+    """Returns :pc:`Shape(width, signed=True)`."""
+    return Shape(width, signed=True)
+
+
+class ShapeCastable:
+    """Interface class for objects that can be cast to a :class:`Shape`.
+
+    Shapes of values in the Amaranth language are specified using :ref:`shape-like objects
+    <lang-shapelike>`. Inheriting a class from :class:`ShapeCastable` and implementing all of
+    the methods described below adds instances of that class to the list of shape-like objects
+    recognized by the :meth:`Shape.cast` method. This is a part of the mechanism for seamlessly
+    extending the Amaranth language in third-party code.
+
+    To illustrate their purpose, consider constructing a signal from a shape-castable object
+    :pc:`shape_castable`:
+
+    .. code::
+
+        value_like = Signal(shape_castable, reset=initializer)
+
+    The code above is equivalent to:
+
+    .. code::
+
+        value_like = shape_castable(Signal(
+            shape_castable.as_shape(),
+            reset=shape_castable.const(initializer)
+        ))
+
+    Note that the :pc:`shape_castable(x)` syntax performs :pc:`shape_castable.__call__(x)`.
+
+    .. tip::
+
+        The source code of the :mod:`amaranth.lib.data` module can be used as a reference for
+        implementing a fully featured shape-castable object.
+    """
+    def __init_subclass__(cls, **kwargs):
+        if cls.as_shape is ShapeCastable.as_shape:
+            raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
+                            f"the 'as_shape' method")
+        if cls.const is ShapeCastable.const:
+            raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
+                            f"the 'const' method")
+        if cls.__call__ is ShapeCastable.__call__:
+            raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
+                            f"the '__call__' method")
+
+    # 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.
+    # This especially applies to `__call__`, where subclasses may call `super().__call__()` in
+    # creative ways.
+
+    def as_shape(self, *args, **kwargs):
+        """as_shape()
+
+        Convert :pc:`self` to a :ref:`shape-like object <lang-shapelike>`.
+
+        This method is called by the Amaranth language to convert :pc:`self` to a concrete
+        :class:`Shape`. It will usually return a :class:`Shape` object, but it may also return
+        another shape-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
+        -------
+        Any other object recognized by :meth:`Shape.cast`.
+
+        Raises
+        ------
+        Exception
+            When the conversion cannot be done. This exception must be propagated by callers
+            (except when checking whether an object is shape-castable or not), either directly
+            or as a cause of another exception.
+        """
+        return super().as_shape(*args, **kwargs) # :nocov:
+
+    def const(self, *args, **kwargs):
+        """const(obj)
+
+        Convert a constant initializer :pc:`obj` to its value representation.
+
+        This method is called by the Amaranth language to convert :pc:`obj`, which may be an
+        arbitrary Python object, to a concrete :ref:`value-like object <lang-valuelike>`.
+        The object :pc:`obj` will usually be a Python literal that can conveniently represent
+        a constant value whose shape is described by :pc:`self`. While not constrained here,
+        the result will usually be an instance of the return type of :meth:`__call__`.
+
+        For any :pc:`obj`, the following condition must hold:
+
+        .. code::
+
+            Shape.cast(self) == Const.cast(self.const(obj)).shape()
+
+        This method may also be called by code that is not a part of the Amaranth language.
+
+        Returns
+        -------
+        A :ref:`value-like object <lang-valuelike>` that is :ref:`constant-castable <lang-constcasting>`.
+
+        Raises
+        ------
+        Exception
+            When the conversion cannot be done. This exception must be propagated by callers,
+            either directly or as a cause of another exception. While not constrained here,
+            usually the exception class will be :exc:`TypeError` or :exc:`ValueError`.
+        """
+        return super().const(*args, **kwargs) # :nocov:
+
+    def __call__(self, *args, **kwargs):
+        """__call__(obj)
+
+        Lift a :ref:`value-like object <lang-valuelike>` to a higher-level representation.
+
+        This method is called by the Amaranth language to lift :pc:`obj`, which may be any
+        :ref:`value-like object <lang-valuelike>` whose shape equals :pc:`Shape.cast(self)`,
+        to a higher-level representation, which may be any value-like object with the same
+        shape. While not constrained here, usually a :class:`ShapeCastable` implementation will
+        be paired with a :class:`ValueCastable` implementation, and this method will return
+        an instance of the latter.
+
+        If :pc:`obj` is not as described above, this interface does not constrain the behavior
+        of this method. This may be used to implement another call-based protocol at the same
+        time.
+
+        For any compliant :pc:`obj`, the following condition must hold:
+
+        .. code::
+
+            Value.cast(self(obj)) == Value.cast(obj)
+
+        This method may also be called by code that is not a part of the Amaranth language.
+
+        Returns
+        -------
+        A :ref:`value-like object <lang-valuelike>`.
+        """
+        return super().__call__(*args, **kwargs) # :nocov:
+
+    # TODO: write an RFC for turning this into a proper interface method
+    def _value_repr(self, value):
+        return (Repr(FormatInt(), value),)
+
+
 class _ShapeLikeMeta(type):
     def __subclasscheck__(cls, subclass):
         return issubclass(subclass, (Shape, ShapeCastable, int, range, EnumMeta)) or subclass is ShapeLike
@@ -173,39 +322,28 @@ class _ShapeLikeMeta(type):
 class ShapeLike(metaclass=_ShapeLikeMeta):
     """An abstract class representing all objects that can be cast to a :class:`Shape`.
 
-    ``issubclass(cls, ShapeLike)`` returns ``True`` for:
+    :pc:`issubclass(cls, ShapeLike)` returns :pc:`True` for:
 
-    - :class:`Shape`
-    - :class:`ShapeCastable` and its subclasses
-    - ``int`` and its subclasses
-    - ``range`` and its subclasses
-    - :class:`enum.EnumMeta` and its subclasses
-    - :class:`ShapeLike` itself
+    * :class:`Shape`;
+    * :class:`ShapeCastable` and its subclasses;
+    * :class:`int` and its subclasses;
+    * :class:`range` and its subclasses;
+    * :class:`enum.EnumMeta` and its subclasses;
+    * :class:`ShapeLike` itself.
 
-    ``isinstance(obj, ShapeLike)`` returns ``True`` for:
+    :pc:`isinstance(obj, ShapeLike)` returns :pc:`True` for:
 
-    - :class:`Shape` instances
-    - :class:`ShapeCastable` instances
-    - non-negative ``int`` values
-    - ``range`` instances
-    - :class:`enum.Enum` subclasses where all values are :ref:`value-like <lang-valuelike>`
+    * :class:`Shape` instances;
+    * :class:`ShapeCastable` instances;
+    * non-negative :class:`int` values;
+    * :class:`range` instances;
+    * :class:`enum.Enum` subclasses where all values are :ref:`value-like objects <lang-valuelike>`.
 
-    This class is only usable for the above checks — no instances and no (non-virtual)
-    subclasses can be created.
+    This class cannot be instantiated or subclassed. It can only be used for checking types of
+    objects.
     """
-
     def __new__(cls, *args, **kwargs):
-        raise TypeError("ShapeLike is an abstract class and cannot be constructed")
-
-
-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)
+        raise TypeError("ShapeLike is an abstract class and cannot be instantiated")
 
 
 def _overridable_by_reflected(method_name):
@@ -701,8 +839,15 @@ class Value(metaclass=ABCMeta):
         raise NotImplementedError # :nocov:
 
 
+class _ConstMeta(ABCMeta):
+    def __call__(cls, value, shape=None, src_loc_at=0, **kwargs):
+        if isinstance(shape, ShapeCastable):
+            return shape.const(value)
+        return super().__call__(value, shape, **kwargs, src_loc_at=src_loc_at + 1)
+
+
 @final
-class Const(Value):
+class Const(Value, metaclass=_ConstMeta):
     """A constant, literal integer value.
 
     Parameters
@@ -760,7 +905,7 @@ class Const(Value):
                             "shape {!r}; this is likely an off-by-one error"
                             .format(self.value, shape),
                     category=SyntaxWarning,
-                    stacklevel=2)
+                    stacklevel=3)
             shape = Shape.cast(shape, src_loc_at=1 + src_loc_at)
         self.width  = shape.width
         self.signed = shape.signed
@@ -782,32 +927,6 @@ class Const(Value):
 C = Const  # shorthand
 
 
-class AnyValue(Value, DUID):
-    def __init__(self, shape, *, src_loc_at=0):
-        super().__init__(src_loc_at=src_loc_at)
-        shape = Shape.cast(shape, src_loc_at=1 + src_loc_at)
-        self.width  = shape.width
-        self.signed = shape.signed
-
-    def shape(self):
-        return Shape(self.width, self.signed)
-
-    def _rhs_signals(self):
-        return SignalSet()
-
-
-@final
-class AnyConst(AnyValue):
-    def __repr__(self):
-        return "(anyconst {}'{})".format(self.width, "s" if self.signed else "")
-
-
-@final
-class AnySeq(AnyValue):
-    def __repr__(self):
-        return "(anyseq {}'{})".format(self.width, "s" if self.signed else "")
-
-
 @final
 class Operator(Value):
     def __init__(self, operator, operands, *, src_loc_at=0):
@@ -1034,37 +1153,6 @@ class Cat(Value):
         return "(cat {})".format(" ".join(map(repr, self.parts)))
 
 
-# TODO(amaranth-0.5): remove
-@deprecated("instead of `Repl(value, count)`, use `value.replicate(count)`")
-def Repl(value, count):
-    """Replicate a value
-
-    An input value is replicated (repeated) several times
-    to be used on the RHS of assignments::
-
-        len(Repl(s, n)) == len(s) * n
-
-    Parameters
-    ----------
-    value : Value, in
-        Input value to be replicated.
-    count : int
-        Number of replications.
-
-    Returns
-    -------
-    Value, out
-        Replicated value.
-    """
-    if isinstance(value, int) and value not in [0, 1]:
-        warnings.warn("Value argument of Repl() is a bare integer {} used in bit vector "
-                        "context; consider specifying explicit width using C({}, {}) instead"
-                        .format(value, value, bits_for(value)),
-                        SyntaxWarning, stacklevel=3)
-
-    return Value.cast(value).replicate(count)
-
-
 class _SignalMeta(ABCMeta):
     def __call__(cls, shape=None, src_loc_at=0, **kwargs):
         signal = super().__call__(shape, **kwargs, src_loc_at=src_loc_at + 1)
@@ -1337,6 +1425,37 @@ class ResetSignal(Value):
         return f"(rst {self.domain})"
 
 
+@final
+class AnyValue(Value, DUID):
+    class Kind(Enum):
+        AnyConst = "anyconst"
+        AnySeq   = "anyseq"
+
+    def __init__(self, kind, shape, *, src_loc_at=0):
+        super().__init__(src_loc_at=src_loc_at)
+        self.kind  = self.Kind(kind)
+        shape = Shape.cast(shape, src_loc_at=1 + src_loc_at)
+        self.width  = shape.width
+        self.signed = shape.signed
+
+    def shape(self):
+        return Shape(self.width, self.signed)
+
+    def _rhs_signals(self):
+        return SignalSet()
+
+    def __repr__(self):
+        return "({} {}'{})".format(self.kind.value, self.width, "s" if self.signed else "")
+
+
+def AnyConst(shape, *, src_loc_at=0):
+    return AnyValue("anyconst", shape, src_loc_at=src_loc_at+1)
+
+
+def AnySeq(shape, *, src_loc_at=0):
+    return AnyValue("anyseq", shape, src_loc_at=src_loc_at+1)
+
+
 class Array(MutableSequence):
     """Addressable multiplexer.
 
@@ -1624,11 +1743,18 @@ class UnusedProperty(UnusedMustUse):
     pass
 
 
+@final
 class Property(Statement, MustUse):
     _MustUse__warning = UnusedProperty
 
-    def __init__(self, test, *, _check=None, _en=None, name=None, src_loc_at=0):
+    class Kind(Enum):
+        Assert = "assert"
+        Assume = "assume"
+        Cover  = "cover"
+
+    def __init__(self, kind, test, *, _check=None, _en=None, name=None, src_loc_at=0):
         super().__init__(src_loc_at=src_loc_at)
+        self.kind   = self.Kind(kind)
         self.test   = Value.cast(test)
         self._check = _check
         self._en    = _en
@@ -1637,10 +1763,10 @@ class Property(Statement, MustUse):
             raise TypeError("Property name must be a string or None, not {!r}"
                             .format(self.name))
         if self._check is None:
-            self._check = Signal(reset_less=True, name=f"${self._kind}$check")
+            self._check = Signal(reset_less=True, name=f"${self.kind.value}$check")
             self._check.src_loc = self.src_loc
         if _en is None:
-            self._en = Signal(reset_less=True, name=f"${self._kind}$en")
+            self._en = Signal(reset_less=True, name=f"${self.kind.value}$en")
             self._en.src_loc = self.src_loc
 
     def _lhs_signals(self):
@@ -1651,23 +1777,20 @@ class Property(Statement, MustUse):
 
     def __repr__(self):
         if self.name is not None:
-            return f"({self.name}: {self._kind} {self.test!r})"
-        return f"({self._kind} {self.test!r})"
+            return f"({self.name}: {self.kind.value} {self.test!r})"
+        return f"({self.kind.value} {self.test!r})"
 
 
-@final
-class Assert(Property):
-    _kind = "assert"
+def Assert(test, *, name=None, src_loc_at=0):
+    return Property("assert", test, name=name, src_loc_at=src_loc_at+1)
 
 
-@final
-class Assume(Property):
-    _kind = "assume"
+def Assume(test, *, name=None, src_loc_at=0):
+    return Property("assume", test, name=name, src_loc_at=src_loc_at+1)
 
 
-@final
-class Cover(Property):
-    _kind = "cover"
+def Cover(test, *, name=None, src_loc_at=0):
+    return Property("cover", test, name=name, src_loc_at=src_loc_at+1)
 
 
 @final
diff --git a/amaranth/hdl/_dsl.py b/amaranth/hdl/_dsl.py
index 01bb6fa..481f468 100644
--- a/amaranth/hdl/_dsl.py
+++ b/amaranth/hdl/_dsl.py
@@ -506,7 +506,7 @@ class Module(_ModuleBuilderRoot, Elaboratable):
             self._pop_ctrl()
 
         for stmt in Statement.cast(assigns):
-            if not isinstance(stmt, (Assign, Assert, Assume, Cover)):
+            if not isinstance(stmt, (Assign, Property)):
                 raise SyntaxError(
                     "Only assignments and property checks may be appended to d.{}"
                     .format(domain_name(domain)))
diff --git a/amaranth/hdl/_xfrm.py b/amaranth/hdl/_xfrm.py
index 900ff46..f23bcba 100644
--- a/amaranth/hdl/_xfrm.py
+++ b/amaranth/hdl/_xfrm.py
@@ -6,7 +6,7 @@ from copy import copy
 from .._utils import flatten, _ignore_deprecated
 from .. import tracer
 from ._ast import *
-from ._ast import _StatementList
+from ._ast import _StatementList, AnyValue, Property
 from ._cd import *
 from ._ir import *
 from ._mem import MemoryInstance
@@ -26,14 +26,6 @@ class ValueVisitor(metaclass=ABCMeta):
     def on_Const(self, value):
         pass # :nocov:
 
-    @abstractmethod
-    def on_AnyConst(self, value):
-        pass # :nocov:
-
-    @abstractmethod
-    def on_AnySeq(self, value):
-        pass # :nocov:
-
     @abstractmethod
     def on_Signal(self, value):
         pass # :nocov:
@@ -46,6 +38,10 @@ class ValueVisitor(metaclass=ABCMeta):
     def on_ResetSignal(self, value):
         pass # :nocov:
 
+    @abstractmethod
+    def on_AnyValue(self, value):
+        pass # :nocov:
+
     @abstractmethod
     def on_Operator(self, value):
         pass # :nocov:
@@ -79,16 +75,14 @@ class ValueVisitor(metaclass=ABCMeta):
     def on_value(self, value):
         if type(value) is Const:
             new_value = self.on_Const(value)
-        elif type(value) is AnyConst:
-            new_value = self.on_AnyConst(value)
-        elif type(value) is AnySeq:
-            new_value = self.on_AnySeq(value)
         elif type(value) is Signal:
             new_value = self.on_Signal(value)
         elif type(value) is ClockSignal:
             new_value = self.on_ClockSignal(value)
         elif type(value) is ResetSignal:
             new_value = self.on_ResetSignal(value)
+        elif type(value) is AnyValue:
+            new_value = self.on_AnyValue(value)
         elif type(value) is Operator:
             new_value = self.on_Operator(value)
         elif type(value) is Slice:
@@ -115,12 +109,6 @@ class ValueTransformer(ValueVisitor):
     def on_Const(self, value):
         return value
 
-    def on_AnyConst(self, value):
-        return value
-
-    def on_AnySeq(self, value):
-        return value
-
     def on_Signal(self, value):
         return value
 
@@ -130,6 +118,9 @@ class ValueTransformer(ValueVisitor):
     def on_ResetSignal(self, value):
         return value
 
+    def on_AnyValue(self, value):
+        return value
+
     def on_Operator(self, value):
         return Operator(value.operator, [self.on_value(o) for o in value.operands])
 
@@ -157,15 +148,7 @@ class StatementVisitor(metaclass=ABCMeta):
         pass # :nocov:
 
     @abstractmethod
-    def on_Assert(self, stmt):
-        pass # :nocov:
-
-    @abstractmethod
-    def on_Assume(self, stmt):
-        pass # :nocov:
-
-    @abstractmethod
-    def on_Cover(self, stmt):
+    def on_Property(self, stmt):
         pass # :nocov:
 
     @abstractmethod
@@ -185,12 +168,8 @@ class StatementVisitor(metaclass=ABCMeta):
     def on_statement(self, stmt):
         if type(stmt) is Assign:
             new_stmt = self.on_Assign(stmt)
-        elif type(stmt) is Assert:
-            new_stmt = self.on_Assert(stmt)
-        elif type(stmt) is Assume:
-            new_stmt = self.on_Assume(stmt)
-        elif type(stmt) is Cover:
-            new_stmt = self.on_Cover(stmt)
+        elif type(stmt) is Property:
+            new_stmt = self.on_Property(stmt)
         elif type(stmt) is Switch:
             new_stmt = self.on_Switch(stmt)
         elif isinstance(stmt, Iterable):
@@ -216,14 +195,8 @@ class StatementTransformer(StatementVisitor):
     def on_Assign(self, stmt):
         return Assign(self.on_value(stmt.lhs), self.on_value(stmt.rhs))
 
-    def on_Assert(self, stmt):
-        return Assert(self.on_value(stmt.test), _check=stmt._check, _en=stmt._en, name=stmt.name)
-
-    def on_Assume(self, stmt):
-        return Assume(self.on_value(stmt.test), _check=stmt._check, _en=stmt._en, name=stmt.name)
-
-    def on_Cover(self, stmt):
-        return Cover(self.on_value(stmt.test), _check=stmt._check, _en=stmt._en, name=stmt.name)
+    def on_Property(self, stmt):
+        return Property(stmt.kind, self.on_value(stmt.test), _check=stmt._check, _en=stmt._en, name=stmt.name)
 
     def on_Switch(self, stmt):
         cases = OrderedDict((k, self.on_statement(s)) for k, s in stmt.cases.items())
@@ -351,9 +324,8 @@ class DomainCollector(ValueVisitor, StatementVisitor):
         pass
 
     on_Const = on_ignore
-    on_AnyConst = on_ignore
-    on_AnySeq = on_ignore
     on_Signal = on_ignore
+    on_AnyValue = on_ignore
 
     def on_ClockSignal(self, value):
         self._add_used_domain(value.domain)
@@ -388,13 +360,9 @@ class DomainCollector(ValueVisitor, StatementVisitor):
         self.on_value(stmt.lhs)
         self.on_value(stmt.rhs)
 
-    def on_property(self, stmt):
+    def on_Property(self, stmt):
         self.on_value(stmt.test)
 
-    on_Assert = on_property
-    on_Assume = on_property
-    on_Cover  = on_property
-
     def on_Switch(self, stmt):
         self.on_value(stmt.test)
         for stmts in stmt.cases.values():
@@ -544,10 +512,8 @@ class SwitchCleaner(StatementVisitor):
     def on_ignore(self, stmt):
         return stmt
 
-    on_Assign = on_ignore
-    on_Assert = on_ignore
-    on_Assume = on_ignore
-    on_Cover  = on_ignore
+    on_Assign   = on_ignore
+    on_Property = on_ignore
 
     def on_Switch(self, stmt):
         cases = OrderedDict((k, self.on_statement(s)) for k, s in stmt.cases.items())
@@ -595,15 +561,11 @@ class LHSGroupAnalyzer(StatementVisitor):
         if lhs_signals:
             self.unify(*stmt._lhs_signals())
 
-    def on_property(self, stmt):
+    def on_Property(self, stmt):
         lhs_signals = stmt._lhs_signals()
         if lhs_signals:
             self.unify(*stmt._lhs_signals())
 
-    on_Assert = on_property
-    on_Assume = on_property
-    on_Cover  = on_property
-
     def on_Switch(self, stmt):
         for case_stmts in stmt.cases.values():
             self.on_statements(case_stmts)
@@ -630,15 +592,11 @@ class LHSGroupFilter(SwitchCleaner):
             if any_lhs_signal in self.signals:
                 return stmt
 
-    def on_property(self, stmt):
+    def on_Property(self, stmt):
         any_lhs_signal = next(iter(stmt._lhs_signals()))
         if any_lhs_signal in self.signals:
             return stmt
 
-    on_Assert = on_property
-    on_Assume = on_property
-    on_Cover  = on_property
-
 
 class _ControlInserter(FragmentTransformer):
     def __init__(self, controls):
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
new file mode 100644
index 0000000..c69f85d
--- /dev/null
+++ b/amaranth/hdl/ast.py
@@ -0,0 +1,32 @@
+# TODO(amaranth-0.6): remove module
+
+from .. import hdl as __hdl
+from . import _ast as __origin
+
+
+__all__ = [
+    "Shape", "signed", "unsigned", "ShapeCastable", "ShapeLike",
+    "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat",
+    "Array", "ArrayProxy",
+    "Signal", "ClockSignal", "ResetSignal",
+    "ValueCastable", "ValueLike",
+    "Initial",
+    "Statement", "Switch",
+    "Property", "Assign", "Assert", "Assume", "Cover",
+    "ValueKey", "ValueDict", "ValueSet", "SignalKey", "SignalDict", "SignalSet",
+]
+
+
+def __getattr__(name):
+    import warnings
+    if name in __hdl.__dict__:
+        if not (name.startswith("__") and name.endswith("__")):
+            warnings.warn(f"instead of `{__name__}.{name}`, use `{__hdl.__name__}.{name}`",
+                        DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    elif name in __origin.__dict__:
+        warnings.warn(f"name `{__name__}.{name}` is a private implementation detail and "
+                      f"should not be imported",
+                      DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/amaranth/hdl/cd.py b/amaranth/hdl/cd.py
new file mode 100644
index 0000000..854a8d2
--- /dev/null
+++ b/amaranth/hdl/cd.py
@@ -0,0 +1,22 @@
+# TODO(amaranth-0.6): remove module
+
+from .. import hdl as __hdl
+from . import _cd as __origin
+
+
+__all__ = ["ClockDomain", "DomainError"]
+
+
+def __getattr__(name):
+    import warnings
+    if name in __hdl.__dict__:
+        if not (name.startswith("__") and name.endswith("__")):
+            warnings.warn(f"instead of `{__name__}.{name}`, use `{__hdl.__name__}.{name}`",
+                        DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    elif name in __origin.__dict__:
+        warnings.warn(f"name `{__name__}.{name}` is a private implementation detail and "
+                      f"should not be imported",
+                      DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/amaranth/hdl/dsl.py b/amaranth/hdl/dsl.py
new file mode 100644
index 0000000..fce61e4
--- /dev/null
+++ b/amaranth/hdl/dsl.py
@@ -0,0 +1,22 @@
+# TODO(amaranth-0.6): remove module
+
+from .. import hdl as __hdl
+from . import _dsl as __origin
+
+
+__all__ = ["SyntaxError", "SyntaxWarning", "Module"]
+
+
+def __getattr__(name):
+    import warnings
+    if name in __hdl.__dict__:
+        if not (name.startswith("__") and name.endswith("__")):
+            warnings.warn(f"instead of `{__name__}.{name}`, use `{__hdl.__name__}.{name}`",
+                        DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    elif name in __origin.__dict__:
+        warnings.warn(f"name `{__name__}.{name}` is a private implementation detail and "
+                      f"should not be imported",
+                      DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/amaranth/hdl/ir.py b/amaranth/hdl/ir.py
new file mode 100644
index 0000000..db44e53
--- /dev/null
+++ b/amaranth/hdl/ir.py
@@ -0,0 +1,22 @@
+# TODO(amaranth-0.6): remove module
+
+from .. import hdl as __hdl
+from . import _ir as __origin
+
+
+__all__ = ["UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "Instance"]
+
+
+def __getattr__(name):
+    import warnings
+    if name in __hdl.__dict__:
+        if not (name.startswith("__") and name.endswith("__")):
+            warnings.warn(f"instead of `{__name__}.{name}`, use `{__hdl.__name__}.{name}`",
+                        DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    elif name in __origin.__dict__:
+        warnings.warn(f"name `{__name__}.{name}` is a private implementation detail and "
+                      f"should not be imported",
+                      DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/amaranth/hdl/mem.py b/amaranth/hdl/mem.py
new file mode 100644
index 0000000..b3db4b0
--- /dev/null
+++ b/amaranth/hdl/mem.py
@@ -0,0 +1,22 @@
+# TODO(amaranth-0.6): remove module
+
+from .. import hdl as __hdl
+from . import _mem as __origin
+
+
+__all__ = ["Memory", "ReadPort", "WritePort", "DummyPort"]
+
+
+def __getattr__(name):
+    import warnings
+    if name in __hdl.__dict__:
+        if not (name.startswith("__") and name.endswith("__")):
+            warnings.warn(f"instead of `{__name__}.{name}`, use `{__hdl.__name__}.{name}`",
+                        DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    elif name in __origin.__dict__:
+        warnings.warn(f"name `{__name__}.{name}` is a private implementation detail and "
+                      f"should not be imported",
+                      DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/amaranth/hdl/xfrm.py b/amaranth/hdl/xfrm.py
new file mode 100644
index 0000000..2b96db6
--- /dev/null
+++ b/amaranth/hdl/xfrm.py
@@ -0,0 +1,28 @@
+# TODO(amaranth-0.6): remove module
+
+from .. import hdl as __hdl
+from . import _xfrm as __origin
+
+
+__all__ = ["ValueVisitor", "ValueTransformer",
+           "StatementVisitor", "StatementTransformer",
+           "FragmentTransformer",
+           "TransformedElaboratable",
+           "DomainCollector", "DomainRenamer", "DomainLowerer",
+           "SwitchCleaner", "LHSGroupAnalyzer", "LHSGroupFilter",
+           "ResetInserter", "EnableInserter"]
+
+
+def __getattr__(name):
+    import warnings
+    if name in __hdl.__dict__:
+        if not (name.startswith("__") and name.endswith("__")):
+            warnings.warn(f"instead of `{__name__}.{name}`, use `{__hdl.__name__}.{name}`",
+                        DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    elif name in __origin.__dict__:
+        warnings.warn(f"name `{__name__}.{name}` is a private implementation detail and "
+                      f"should not be imported",
+                      DeprecationWarning, stacklevel=2)
+        return getattr(__origin, name)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/amaranth/lib/fifo.py b/amaranth/lib/fifo.py
index b2b542f..48023bb 100644
--- a/amaranth/lib/fifo.py
+++ b/amaranth/lib/fifo.py
@@ -130,9 +130,9 @@ class SyncFIFO(Elaboratable, FIFOInterface):
         do_read  = self.r_rdy & self.r_en
         do_write = self.w_rdy & self.w_en
 
-        storage = Memory(width=self.width, depth=self.depth)
-        w_port  = m.submodules.w_port = storage.write_port()
-        r_port  = m.submodules.r_port = storage.read_port(domain="comb")
+        storage = m.submodules.storage = Memory(width=self.width, depth=self.depth)
+        w_port  = storage.write_port()
+        r_port  = storage.read_port(domain="comb")
         produce = Signal(range(self.depth))
         consume = Signal(range(self.depth))
 
@@ -257,7 +257,7 @@ class SyncFIFOBuffered(Elaboratable, FIFOInterface):
 
         do_inner_read  = inner_r_rdy & (~self.r_rdy | self.r_en)
 
-        m.submodules.storage = storage = Memory(width=self.width, depth=inner_depth)
+        storage = m.submodules.storage = Memory(width=self.width, depth=inner_depth)
         w_port  = storage.write_port()
         r_port  = storage.read_port(domain="sync", transparent=False)
         produce = Signal(range(inner_depth))
@@ -438,10 +438,9 @@ class AsyncFIFO(Elaboratable, FIFOInterface):
         m.d[self._w_domain] += self.w_level.eq(produce_w_bin - consume_w_bin)
         m.d.comb += self.r_level.eq(produce_r_bin - consume_r_bin)
 
-        storage = Memory(width=self.width, depth=self.depth)
-        w_port  = m.submodules.w_port = storage.write_port(domain=self._w_domain)
-        r_port  = m.submodules.r_port = storage.read_port (domain=self._r_domain,
-                                                           transparent=False)
+        storage = m.submodules.storage = Memory(width=self.width, depth=self.depth)
+        w_port  = storage.write_port(domain=self._w_domain)
+        r_port  = storage.read_port (domain=self._r_domain, transparent=False)
         m.d.comb += [
             w_port.addr.eq(produce_w_bin[:-1]),
             w_port.data.eq(self.w_data),
diff --git a/amaranth/sim/_pyrtl.py b/amaranth/sim/_pyrtl.py
index 3862593..121804d 100644
--- a/amaranth/sim/_pyrtl.py
+++ b/amaranth/sim/_pyrtl.py
@@ -97,10 +97,7 @@ class _ValueCompiler(ValueVisitor, _Compiler):
     def on_ResetSignal(self, value):
         raise NotImplementedError # :nocov:
 
-    def on_AnyConst(self, value):
-        raise NotImplementedError # :nocov:
-
-    def on_AnySeq(self, value):
+    def on_AnyValue(self, value):
         raise NotImplementedError # :nocov:
 
     def on_Initial(self, value):
@@ -389,13 +386,7 @@ class _StatementCompiler(StatementVisitor, _Compiler):
             with self.emitter.indent():
                 self(stmts)
 
-    def on_Assert(self, stmt):
-        raise NotImplementedError # :nocov:
-
-    def on_Assume(self, stmt):
-        raise NotImplementedError # :nocov:
-
-    def on_Cover(self, stmt):
+    def on_Property(self, stmt):
         raise NotImplementedError # :nocov:
 
     @classmethod
diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py
index aac3ab0..0fa9ea0 100644
--- a/amaranth/sim/pysim.py
+++ b/amaranth/sim/pysim.py
@@ -65,6 +65,7 @@ class _VCDWriter:
         trace_names = SignalDict()
         assigned_names = set()
         for trace in traces:
+            trace = Value.cast(trace)
             for trace_signal in trace._rhs_signals():
                 if trace_signal not in signal_names:
                     if trace_signal.name not in assigned_names:
diff --git a/docs/changes.rst b/docs/changes.rst
index 9cdf331..bc25118 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -36,9 +36,11 @@ Implemented RFCs
 
 .. _RFC 17: https://amaranth-lang.org/rfcs/0017-remove-log2-int.html
 .. _RFC 39: https://amaranth-lang.org/rfcs/0039-empty-case.html
+.. _RFC 46: https://amaranth-lang.org/rfcs/0046-shape-range-1.html
 
 * `RFC 17`_: Remove ``log2_int``
 * `RFC 39`_: Change semantics of no-argument ``m.Case()``
+* `RFC 46`_: Change ``Shape.cast(range(1))`` to ``unsigned(0)``
 
 
 Language changes
@@ -52,8 +54,10 @@ Language changes
 * Changed: ``Value.matches()`` with no patterns is ``Const(0)`` instead of ``Const(1)``. (`RFC 39`_)
 * Changed: ``Signal(range(stop), reset=stop)`` warning has been changed into a hard error and made to trigger on any out-of range value.
 * Changed: ``Signal(range(0))`` is now valid without a warning.
+* Changed: ``Shape.cast(range(1))`` is now ``unsigned(0)``. (`RFC 46`_)
 * Deprecated: :func:`amaranth.utils.log2_int`. (`RFC 17`_)
 * Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_)
+* Removed: (deprecated in 0.4) :class:`Repl`. (`RFC 10`_)
 * Removed: (deprecated in 0.4) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`.
 
 
diff --git a/docs/lang.rst b/docs/guide.rst
similarity index 97%
rename from docs/lang.rst
rename to docs/guide.rst
index 81b9e57..ccd611d 100644
--- a/docs/lang.rst
+++ b/docs/guide.rst
@@ -6,11 +6,13 @@
 Language guide
 ##############
 
+.. py:currentmodule:: amaranth.hdl
+
 .. warning::
 
    This guide is a work in progress and is seriously incomplete!
 
-This guide introduces the Amaranth language in depth. It assumes familiarity with synchronous digital logic and the Python programming language, but does not require prior experience with any hardware description language. See the :doc:`tutorial <tutorial>` for a step-by-step introduction to the language.
+This guide introduces the Amaranth language in depth. It assumes familiarity with synchronous digital logic and the Python programming language, but does not require prior experience with any hardware description language. See the :doc:`tutorial <tutorial>` for a step-by-step introduction to the language, and the :doc:`reference <reference>` for a detailed description of the Python classes that underlie the language's syntax.
 
 .. TODO: link to a good synchronous logic tutorial and a Python tutorial?
 
@@ -44,7 +46,7 @@ All of the examples below assume that a glob import is used.
 Shapes
 ======
 
-A ``Shape`` is an object with two attributes, ``.width`` and ``.signed``. It can be constructed directly:
+A :class:`Shape` describes the bit width and signedness of an Amaranth value. It can be constructed directly:
 
 .. doctest::
 
@@ -53,7 +55,7 @@ A ``Shape`` is an object with two attributes, ``.width`` and ``.signed``. It can
    >>> Shape(width=12, signed=True)
    signed(12)
 
-However, in most cases, the shape is always constructed with the same signedness, and the aliases ``signed`` and ``unsigned`` are more convenient:
+However, in most cases, the signedness of a shape is known upfront, and the convenient aliases :func:`signed` and :func:`unsigned` can be used:
 
 .. doctest::
 
@@ -126,9 +128,9 @@ The shape of the constant can be specified explicitly, in which case the number'
 Shape casting
 =============
 
-Shapes can be *cast* from other objects, which are called *shape-like*. Casting is a convenient way to specify a shape indirectly, for example, by a range of numbers representable by values with that shape.
+Shapes can be *cast* from other objects, which are called *shape-like*. Casting is a convenient way to specify a shape indirectly, for example, by a range of numbers representable by values with that shape. Shapes are shape-like objects as well.
 
-Casting to a shape can be done explicitly with ``Shape.cast``, but is usually implicit, since shape-like objects are accepted anywhere shapes are.
+Casting to a shape can be done explicitly with :meth:`Shape.cast`, but is usually implicit, since shape-like objects are accepted anywhere shapes are.
 
 
 .. _lang-shapeint:
@@ -136,7 +138,7 @@ Casting to a shape can be done explicitly with ``Shape.cast``, but is usually im
 Shapes from integers
 --------------------
 
-Casting a shape from an integer ``i`` is a shorthand for constructing a shape with ``unsigned(i)``:
+Casting a shape from an integer ``i`` is a shorthand for constructing a shape with :func:`unsigned(i) <unsigned>`:
 
 .. doctest::
 
@@ -186,7 +188,7 @@ Specifying a shape with a range is convenient for counters, indexes, and all oth
 
 .. note::
 
-   An empty range always casts to an ``unsigned(0)``, even if both of its bounds are negative.
+   An empty range always casts to an :py:`unsigned(0)`, even if both of its bounds are negative.
    This happens because, being empty, it does not contain any negative values.
 
    .. doctest::
@@ -244,22 +246,30 @@ The :mod:`amaranth.lib.enum` module extends the standard enumerations such that
    The enumeration does not have to subclass :class:`enum.IntEnum` or have :class:`int` as one of its base classes; it only needs to have integers as values of every member. Using enumerations based on :class:`enum.Enum` rather than :class:`enum.IntEnum` prevents unwanted implicit conversion of enum members to integers.
 
 
+.. _lang-shapecustom:
+
+Custom shapes
+-------------
+
+Any Python value that implements the :class:`ShapeCastable` interface can extend the language with a custom shape-like object. For example, the standard library module :mod:`amaranth.lib.data` uses this facility to add support for aggregate data types to the language.
+
+
 .. _lang-valuelike:
 
 Value casting
 =============
 
-Like shapes, values may be *cast* from other objects, which are called *value-like*. Casting to values allows objects that are not provided by Amaranth, such as integers or enumeration members, to be used in Amaranth expressions directly.
+Like shapes, values may be *cast* from other objects, which are called *value-like*. Casting to values allows objects that are not provided by Amaranth, such as integers or enumeration members, to be used in Amaranth expressions directly. Values are value-like objects as well.
 
 .. TODO: link to ValueCastable
 
-Casting to a value can be done explicitly with ``Value.cast``, but is usually implicit, since value-like objects are accepted anywhere values are.
+Casting to a value can be done explicitly with :meth:`Value.cast`, but is usually implicit, since value-like objects are accepted anywhere values are.
 
 
 Values from integers
 --------------------
 
-Casting a value from an integer ``i`` is equivalent to ``Const(i)``:
+Casting a value from an integer ``i`` is equivalent to :class:`Const(i) <Const>`:
 
 .. doctest::
 
@@ -344,7 +354,7 @@ A *signal* is a value representing a (potentially) varying number. Signals can b
 Signal shapes
 -------------
 
-A signal can be created with an explicitly specified shape (any :ref:`shape-like <lang-shapelike>` object); if omitted, the shape defaults to ``unsigned(1)``. Although rarely useful, 0-bit signals are permitted.
+A signal can be created with an explicitly specified shape (any :ref:`shape-like <lang-shapelike>` object); if omitted, the shape defaults to :func:`unsigned(1) <unsigned>`. Although rarely useful, 0-bit signals are permitted.
 
 .. doctest::
 
diff --git a/docs/index.rst b/docs/index.rst
index 0e6e0df..0228708 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -12,7 +12,8 @@ Language & toolchain
    install
    start
    tutorial
-   lang
+   guide
+   reference
    stdlib
    platform
    changes
diff --git a/docs/intro.rst b/docs/intro.rst
index 2fed044..33ec2f2 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -16,7 +16,7 @@ The Amaranth toolchain consists of the :ref:`Amaranth language <intro-lang>`, th
 The Amaranth language
 =====================
 
-The :doc:`Amaranth hardware description language <lang>` is a Python library for register transfer level modeling of synchronous logic. Ordinary Python code is used to construct a netlist of a digital circuit, which can be simulated, directly synthesized via Yosys_, or converted to human-readable Verilog code for use with industry-standard toolchains.
+The :doc:`Amaranth hardware description language <guide>` is a Python library for register transfer level modeling of synchronous logic. Ordinary Python code is used to construct a netlist of a digital circuit, which can be simulated, directly synthesized via Yosys_, or converted to human-readable Verilog code for use with industry-standard toolchains.
 
 By relying on the flexibility, rich functionality and widespread adoption of the Python language, the Amaranth language is focused on a single task: modeling digital logic well. It has first-class support for building blocks like clock domains and finite state machines, and uses simple rules for arithmetic operations that closely match the Python semantics. Python classes, functions, loops and conditionals can be used to build organized and flexible designs; Python libraries can be seamlessly used with Amaranth during design or verification; and Python development tools can process Amaranth code.
 
diff --git a/docs/reference.rst b/docs/reference.rst
new file mode 100644
index 0000000..dfc5a55
--- /dev/null
+++ b/docs/reference.rst
@@ -0,0 +1,105 @@
+Language reference
+##################
+
+.. py:module:: amaranth.hdl
+
+.. warning::
+
+    This reference is a work in progress and is seriously incomplete!
+
+    While the wording below states that anything not described in this document isn't covered by the backwards compatibility guarantee, this should be ignored until the document is complete and this warning is removed.
+
+This reference describes the Python classes that underlie the Amaranth language's syntax. It assumes familiarity with the :doc:`language guide <guide>`.
+
+
+.. _lang-stability:
+
+Backwards compatibility
+=======================
+
+As part of the Amaranth backwards compatibility guarantee, any behaviors described in this document will not change from a version to another without at least one version including a warning about the impending change. Any nontrivial change to these behaviors must also go through the public review as a part of the `Amaranth Request for Comments process <https://amaranth-lang.org/rfcs/>`_.
+
+Conversely, any behavior not documented here is subject to change at any time with or without notice, and any names under the :mod:`amaranth.hdl` module that are not explicitly included in this document, even if they do not begin with an underscore, are internal to the implementation of the language.
+
+
+.. _lang-importing:
+
+Importing syntax
+================
+
+There are two ways to import the Amaranth syntax into a Python file: by importing the :ref:`prelude <lang-prelude>` or by importing individual names from the :mod:`amaranth.hdl` module. Since the prelude is kept small and rarely extended to avoid breaking downstream code that uses a glob import, there are some names that are only exported from the :mod:`amaranth.hdl` module. The following three snippets are equivalent:
+
+.. testcode::
+
+    from amaranth import *
+
+    m = Module()
+
+.. testcode::
+
+    import amaranth as am
+
+    m = am.Module()
+
+.. testcode::
+
+    from amaranth.hdl import Module
+
+    m = Module()
+
+The prelude exports exactly the following names:
+
+.. must be kept in sync with amaranth/__init__.py!
+
+* :class:`Shape`
+* :func:`unsigned`
+* :func:`signed`
+* :class:`Value`
+* :class:`Const`
+* :func:`C`
+* :func:`Mux`
+* :class:`Cat`
+* :class:`Array`
+* :class:`Signal`
+* :class:`ClockSignal`
+* :class:`ResetSignal`
+* :class:`Module`
+* :class:`ClockDomain`
+* :class:`Elaboratable`
+* :class:`Fragment`
+* :class:`Instance`
+* :class:`Memory`
+* :class:`Record` (deprecated)
+* :class:`DomainRenamer`
+* :class:`ResetInserter`
+* :class:`EnableInserter`
+
+
+.. _lang-srcloc:
+
+Source locations
+================
+
+Many functions and methods in Amaranth take the :pc:`src_loc_at=0` keyword argument. These language constructs may inspect the call stack to determine the file and line of its call site, which will be used to annotate generated code when a netlist is generated or to improve diagnostic messages.
+
+Some call sites are not relevant for an Amaranth designer; e.g. when an Amaranth language construct is called from a user-defined utility function, the source location of the call site within this utility function is usually not interesting to the designer. In these cases, one or more levels of function calls can be removed from consideration using the :pc:`src_loc_at` argument as follows (using :meth:`Shape.cast` to demonstrate the concept):
+
+.. testcode::
+
+    def my_shape_cast(obj, *, src_loc_at=0):
+        ... # additionally process `obj`...
+        return Shape.cast(obj, src_loc_at=1 + src_loc_at)
+
+The number :pc:`1` corresponds to the number of call stack frames that should be skipped.
+
+
+Shapes
+======
+
+See also the introduction to :ref:`shapes <lang-shapes>` and :ref:`casting from shape-like objects <lang-shapelike>` in the language guide.
+
+.. autoclass:: Shape
+.. autofunction:: unsigned
+.. autofunction:: signed
+.. autoclass:: ShapeCastable
+.. autoclass:: ShapeLike
diff --git a/docs/start.rst b/docs/start.rst
index 0be3a76..84a9aa1 100644
--- a/docs/start.rst
+++ b/docs/start.rst
@@ -1,7 +1,7 @@
 Getting started
 ###############
 
-This section demonstrates the basic Amaranth workflow to provide a cursory overview of the language and the toolchain. See the :doc:`tutorial <tutorial>` for a step-by-step introduction to the language, and the :doc:`language guide <lang>` for a detailed explanation of every language construct.
+This section demonstrates the basic Amaranth workflow to provide a cursory overview of the language and the toolchain. See the :doc:`tutorial <tutorial>` for a step-by-step introduction to the language, and the :doc:`language guide <guide>` for a detailed explanation of every language construct.
 
 .. TODO: add link to build system doc
 .. TODO: add link to more complex examples?
diff --git a/docs/stdlib.rst b/docs/stdlib.rst
index 1f3ab73..2badf81 100644
--- a/docs/stdlib.rst
+++ b/docs/stdlib.rst
@@ -7,6 +7,8 @@ The :mod:`amaranth.lib` module, also known as the standard library, provides mod
 2. Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:`amaranth.lib.cdc`.
 3. Modules that have essentially one correct implementation and are of broad utility in digital designs. This includes :mod:`amaranth.lib.coding`, :mod:`amaranth.lib.fifo`, and :mod:`amaranth.lib.crc`.
 
+As part of the Amaranth backwards compatibility guarantee, any behaviors described in these documents will not change from a version to another without at least one version including a warning about the impending change. Any nontrivial change to these behaviors must also go through the public review as a part of the `Amaranth Request for Comments process <https://amaranth-lang.org/rfcs/>`_.
+
 The Amaranth standard library is separate from the Amaranth language: everything provided in it could have been implemented in a third-party library.
 
 .. toctree::


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index a670f58..8829aa7 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -115,7 +115,7 @@ class ShapeTestCase(FHDLTestCase):
         self.assertEqual(s3.width, 4)
         self.assertEqual(s3.signed, True)
         s4 = Shape.cast(range(0, 1))
-        self.assertEqual(s4.width, 1)
+        self.assertEqual(s4.width, 0)
         self.assertEqual(s4.signed, False)
         s5 = Shape.cast(range(-1, 0))
         self.assertEqual(s5.width, 1)
@@ -129,6 +129,9 @@ class ShapeTestCase(FHDLTestCase):
         s8 = Shape.cast(range(0, 10, 3))
         self.assertEqual(s8.width, 4)
         self.assertEqual(s8.signed, False)
+        s9 = Shape.cast(range(0, 3, 3))
+        self.assertEqual(s9.width, 0)
+        self.assertEqual(s9.signed, False)
 
     def test_cast_enum(self):
         s1 = Shape.cast(UnsignedEnum)
@@ -167,8 +170,8 @@ class MockShapeCastable(ShapeCastable):
 class ShapeCastableTestCase(FHDLTestCase):
     def test_no_override(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockShapeCastableNoOverride' deriving from `ShapeCastable` must "
-                r"override the `as_shape` method$"):
+                r"^Class 'MockShapeCastableNoOverride' deriving from 'ShapeCastable' must "
+                r"override the 'as_shape' method$"):
             class MockShapeCastableNoOverride(ShapeCastable):
                 def __init__(self):
                     pass
@@ -467,6 +470,25 @@ class ConstTestCase(FHDLTestCase):
         with self.assertRaises(TypeError):
             hash(Const(0))
 
+    def test_shape_castable(self):
+        class MockConstValue:
+            def __init__(self, value):
+                self.value = value
+
+        class MockConstShape(ShapeCastable):
+            def as_shape(self):
+                return unsigned(8)
+
+            def __call__(self, value):
+                return value
+
+            def const(self, init):
+                return MockConstValue(init)
+
+        s = Const(10, MockConstShape())
+        self.assertIsInstance(s, MockConstValue)
+        self.assertEqual(s.value, 10)
+
 
 class OperatorTestCase(FHDLTestCase):
     def test_bool(self):
@@ -970,27 +992,6 @@ class CatTestCase(FHDLTestCase):
         self.assertEqual(a.signed, False)
 
 
-class ReplTestCase(FHDLTestCase):
-    @_ignore_deprecated
-    def test_cast(self):
-        r = Repl(0, 3)
-        self.assertEqual(repr(r), "(cat (const 1'd0) (const 1'd0) (const 1'd0))")
-
-    @_ignore_deprecated
-    def test_int_01(self):
-        with warnings.catch_warnings():
-            warnings.filterwarnings(action="error", category=SyntaxWarning)
-            Repl(0, 3)
-            Repl(1, 3)
-
-    @_ignore_deprecated
-    def test_int_wrong(self):
-        with self.assertWarnsRegex(SyntaxWarning,
-                r"^Value argument of Repl\(\) is a bare integer 2 used in bit vector context; "
-                r"consider specifying explicit width using C\(2, 2\) instead$"):
-            Repl(2, 3)
-
-
 class ArrayTestCase(FHDLTestCase):
     def test_acts_like_array(self):
         a = Array([1,2,3])
@@ -1109,7 +1110,7 @@ class SignalTestCase(FHDLTestCase):
         s10 = Signal(range(0))
         self.assertEqual(s10.shape(), unsigned(0))
         s11 = Signal(range(1))
-        self.assertEqual(s11.shape(), unsigned(1))
+        self.assertEqual(s11.shape(), unsigned(0))
 
     def test_shape_wrong(self):
         with self.assertRaisesRegex(TypeError,