docs/reference: document compat guarantee, importing, shapes.

This commit also contains a related semantic change: it adds `Shape`
and `ShapeCastable` to the `__all__` list in `amaranth.hdl`. This is
consistent with the policy that is laid out in the new documentation,
which permits such additions without notice.

Co-authored-by: mcclure <mcclure@users.noreply.github.com>
This commit is contained in:
Catherine 2024-01-30 01:44:07 +00:00
parent c9b87a4fc5
commit ea3d6c9557
6 changed files with 325 additions and 79 deletions

View file

@ -12,7 +12,7 @@ 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",

View file

@ -1,5 +1,4 @@
from abc import ABCMeta, abstractmethod
import inspect
import warnings
import functools
import operator
@ -12,7 +11,6 @@ from ._repr import *
from .. import tracer
from ..utils import *
from .._utils import *
from .._utils import _ignore_deprecated
from .._unused import *
@ -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 is wide enough to represent any element 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
@ -142,6 +133,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 +147,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 +320,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):

View file

@ -6,6 +6,8 @@
Language guide
##############
.. py:currentmodule:: amaranth.hdl
.. warning::
This guide is a work in progress and is seriously incomplete!
@ -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::

View file

@ -1,8 +1,106 @@
Language reference
##################
.. py:module:: amaranth.hdl
.. warning::
This guide is a work in progress and is seriously incomplete!
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:`Repl` (deprecated)
* :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

View file

@ -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::

View file

@ -167,8 +167,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