Skip to content

Commit 35a3a9e

Browse files
whitequarkmcclure
andcommitted
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>
1 parent c93aa42 commit 35a3a9e

File tree

7 files changed

+329
-83
lines changed

7 files changed

+329
-83
lines changed

amaranth/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .hdl import *
1414

15-
15+
# must be kept in sync with docs/reference.rst!
1616
__all__ = [
1717
"Shape", "unsigned", "signed",
1818
"Value", "Const", "C", "Mux", "Cat", "Repl", "Array", "Signal", "ClockSignal", "ResetSignal",

amaranth/hdl/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import warnings
22

3-
from .ast import Shape, unsigned, signed
3+
from .ast import Shape, unsigned, signed, ShapeCastable, ShapeLike
44
from .ast import Value, Const, C, Mux, Cat, Repl, Array, Signal, ClockSignal, ResetSignal
55
from .dsl import Module
66
from .cd import ClockDomain
@@ -13,7 +13,7 @@
1313

1414

1515
__all__ = [
16-
"Shape", "unsigned", "signed",
16+
"Shape", "unsigned", "signed", "ShapeCastable", "ShapeLike",
1717
"Value", "Const", "C", "Mux", "Cat", "Repl", "Array", "Signal", "ClockSignal", "ResetSignal",
1818
"Module",
1919
"ClockDomain",

amaranth/hdl/ast.py

Lines changed: 201 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from abc import ABCMeta, abstractmethod
2-
import inspect
32
import warnings
43
import functools
54
import operator
@@ -12,7 +11,6 @@
1211
from .. import tracer
1312
from ..utils import *
1413
from .._utils import *
15-
from .._utils import _ignore_deprecated
1614
from .._unused import *
1715

1816

@@ -37,51 +35,23 @@ def __init__(self):
3735
DUID.__next_uid += 1
3836

3937

40-
class ShapeCastable:
41-
"""Interface of user-defined objects that can be cast to :class:`Shape` s.
42-
43-
An object deriving from :class:`ShapeCastable` is automatically converted to a :class:`Shape`
44-
when it is used in a context where a :class:`Shape` is expected. Such objects can contain
45-
a richer description of the shape than what is supported by the core Amaranth language, yet
46-
still be transparently used with it.
47-
"""
48-
def __init_subclass__(cls, **kwargs):
49-
if not hasattr(cls, "as_shape"):
50-
raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
51-
f"the `as_shape` method")
52-
if not (hasattr(cls, "__call__") and inspect.isfunction(cls.__call__)):
53-
raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
54-
f"the `__call__` method")
55-
if not hasattr(cls, "const"):
56-
raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
57-
f"the `const` method")
58-
59-
def _value_repr(self, value):
60-
return (Repr(FormatInt(), value),)
61-
62-
6338
class Shape:
6439
"""Bit width and signedness of a value.
6540
66-
A ``Shape`` can be constructed using:
67-
* explicit bit width and signedness;
68-
* aliases :func:`signed` and :func:`unsigned`;
69-
* casting from a variety of objects.
41+
A :class:`Shape` can be obtained by:
7042
71-
A ``Shape`` can be cast from:
72-
* an integer, where the integer specifies the bit width;
73-
* a range, where the result is wide enough to represent any element of the range, and is
74-
signed if any element of the range is signed;
75-
* an :class:`Enum` with all integer members or :class:`IntEnum`, where the result is wide
76-
enough to represent any member of the enumeration, and is signed if any member of
77-
the enumeration is signed.
43+
* constructing with explicit bit width and signedness;
44+
* using the :func:`signed` and :func:`unsigned` aliases if the signedness is known upfront;
45+
* casting from a variety of objects using the :meth:`cast` method.
7846
7947
Parameters
8048
----------
8149
width : int
82-
The number of bits in the representation, including the sign bit (if any).
50+
The number of bits in the representation of a value. This includes the sign bit for signed
51+
values. Cannot be zero if the value is signed.
8352
signed : bool
84-
If ``False``, the value is unsigned. If ``True``, the value is signed two's complement.
53+
Whether the value is signed. Signed values use the
54+
`two's complement <https://en.wikipedia.org/wiki/Two's_complement>`_ representation.
8555
"""
8656
def __init__(self, width=1, signed=False):
8757
if not isinstance(width, int):
@@ -117,6 +87,27 @@ def _cast_plain_enum(obj):
11787

11888
@staticmethod
11989
def cast(obj, *, src_loc_at=0):
90+
"""Cast :pc:`obj` to a shape.
91+
92+
Many :ref:`shape-like <lang-shapelike>` objects can be cast to a shape:
93+
94+
* a :class:`Shape`, where the result is itself;
95+
* an :class:`int`, where the result is :func:`unsigned(obj) <unsigned>`;
96+
* a :class:`range`, where the result is wide enough to represent any element of the range,
97+
and is signed if any element of the range is signed;
98+
* an :class:`enum.Enum` whose members are all :ref:`constant-castable <lang-constcasting>`
99+
or :class:`enum.IntEnum`, where the result is wide enough to represent any member of
100+
the enumeration, and is signed if any member of the enumeration is signed;
101+
* a :class:`ShapeCastable` object, where the result is obtained by repeatedly calling
102+
:meth:`obj.as_shape() <ShapeCastable.as_shape>`.
103+
104+
Raises
105+
------
106+
TypeError
107+
If :pc:`obj` cannot be converted to a :class:`Shape`.
108+
RecursionError
109+
If :pc:`obj` is a :class:`ShapeCastable` object that casts to itself.
110+
"""
120111
while True:
121112
if isinstance(obj, Shape):
122113
return obj
@@ -142,6 +133,10 @@ def cast(obj, *, src_loc_at=0):
142133
obj = new_obj
143134

144135
def __repr__(self):
136+
"""Python code that creates this shape.
137+
138+
Returns :pc:`f"signed({self.width})"` or :pc:`f"unsigned({self.width})"`.
139+
"""
145140
if self.signed:
146141
return f"signed({self.width})"
147142
else:
@@ -152,6 +147,158 @@ def __eq__(self, other):
152147
self.width == other.width and self.signed == other.signed)
153148

154149

150+
def unsigned(width):
151+
"""Returns :pc:`Shape(width, signed=False)`."""
152+
return Shape(width, signed=False)
153+
154+
155+
def signed(width):
156+
"""Returns :pc:`Shape(width, signed=True)`."""
157+
return Shape(width, signed=True)
158+
159+
160+
class ShapeCastable:
161+
"""Interface class for objects that can be cast to a :class:`Shape`.
162+
163+
Shapes of values in the Amaranth language are specified using :ref:`shape-like objects
164+
<lang-shapelike>`. Inheriting a class from :class:`ShapeCastable` and implementing all of
165+
the methods described below adds instances of that class to the list of shape-like objects
166+
recognized by the :meth:`Shape.cast` method. This is a part of the mechanism for seamlessly
167+
extending the Amaranth language in third-party code.
168+
169+
To illustrate their purpose, consider constructing a signal from a shape-castable object
170+
:pc:`shape_castable`:
171+
172+
.. code::
173+
174+
value_like = Signal(shape_castable, reset=initializer)
175+
176+
The code above is equivalent to:
177+
178+
.. code::
179+
180+
value_like = shape_castable(Signal(
181+
shape_castable.as_shape(),
182+
reset=shape_castable.const(initializer)
183+
))
184+
185+
Note that the :pc:`shape_castable(x)` syntax performs :pc:`shape_castable.__call__(x)`.
186+
187+
.. tip::
188+
189+
The source code of the :mod:`amaranth.lib.data` module can be used as a reference for
190+
implementing a fully featured shape-castable object.
191+
"""
192+
def __init_subclass__(cls, **kwargs):
193+
if cls.as_shape is ShapeCastable.as_shape:
194+
raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
195+
f"the 'as_shape' method")
196+
if cls.const is ShapeCastable.const:
197+
raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
198+
f"the 'const' method")
199+
if cls.__call__ is ShapeCastable.__call__:
200+
raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
201+
f"the '__call__' method")
202+
203+
# The signatures and definitions of these methods are weird because they are present here for
204+
# documentation (and error checking above) purpose only and should not affect control flow.
205+
# This especially applies to `__call__`, where subclasses may call `super().__call__()` in
206+
# creative ways.
207+
208+
def as_shape(self, *args, **kwargs):
209+
"""as_shape()
210+
211+
Convert :pc:`self` to a :ref:`shape-like object <lang-shapelike>`.
212+
213+
This method is called by the Amaranth language to convert :pc:`self` to a concrete
214+
:class:`Shape`. It will usually return a :class:`Shape` object, but it may also return
215+
another shape-like object to delegate its functionality.
216+
217+
This method must be idempotent: when called twice on the same object, the result must be
218+
exactly the same.
219+
220+
This method may also be called by code that is not a part of the Amaranth language.
221+
222+
Returns
223+
-------
224+
Any other object recognized by :meth:`Shape.cast`.
225+
226+
Raises
227+
------
228+
Exception
229+
When the conversion cannot be done. This exception must be propagated by callers
230+
(except when checking whether an object is shape-castable or not), either directly
231+
or as a cause of another exception.
232+
"""
233+
return super().as_shape(*args, **kwargs) # :nocov:
234+
235+
def const(self, *args, **kwargs):
236+
"""const(obj)
237+
238+
Convert a constant initializer :pc:`obj` to its value representation.
239+
240+
This method is called by the Amaranth language to convert :pc:`obj`, which may be an
241+
arbitrary Python object, to a concrete :ref:`value-like object <lang-valuelike>`.
242+
The object :pc:`obj` will usually be a Python literal that can conveniently represent
243+
a constant value whose shape is described by :pc:`self`. While not constrained here,
244+
the result will usually be an instance of the return type of :meth:`__call__`.
245+
246+
For any :pc:`obj`, the following condition must hold:
247+
248+
.. code::
249+
250+
Shape.cast(self) == Const.cast(self.const(obj)).shape()
251+
252+
This method may also be called by code that is not a part of the Amaranth language.
253+
254+
Returns
255+
-------
256+
A :ref:`value-like object <lang-valuelike>` that is :ref:`constant-castable <lang-constcasting>`.
257+
258+
Raises
259+
------
260+
Exception
261+
When the conversion cannot be done. This exception must be propagated by callers,
262+
either directly or as a cause of another exception. While not constrained here,
263+
usually the exception class will be :exc:`TypeError` or :exc:`ValueError`.
264+
"""
265+
return super().const(*args, **kwargs) # :nocov:
266+
267+
def __call__(self, *args, **kwargs):
268+
"""__call__(obj)
269+
270+
Lift a :ref:`value-like object <lang-valuelike>` to a higher-level representation.
271+
272+
This method is called by the Amaranth language to lift :pc:`obj`, which may be any
273+
:ref:`value-like object <lang-valuelike>` whose shape equals :pc:`Shape.cast(self)`,
274+
to a higher-level representation, which may be any value-like object with the same
275+
shape. While not constrained here, usually a :class:`ShapeCastable` implementation will
276+
be paired with a :class:`ValueCastable` implementation, and this method will return
277+
an instance of the latter.
278+
279+
If :pc:`obj` is not as described above, this interface does not constrain the behavior
280+
of this method. This may be used to implement another call-based protocol at the same
281+
time.
282+
283+
For any compliant :pc:`obj`, the following condition must hold:
284+
285+
.. code::
286+
287+
Value.cast(self(obj)) == Value.cast(obj)
288+
289+
This method may also be called by code that is not a part of the Amaranth language.
290+
291+
Returns
292+
-------
293+
A :ref:`value-like object <lang-valuelike>`.
294+
"""
295+
return super().__call__(*args, **kwargs) # :nocov:
296+
297+
# TODO: write an RFC for turning this into a proper interface method
298+
def _value_repr(self, value):
299+
return (Repr(FormatInt(), value),)
300+
301+
155302
class _ShapeLikeMeta(type):
156303
def __subclasscheck__(cls, subclass):
157304
return issubclass(subclass, (Shape, ShapeCastable, int, range, EnumMeta)) or subclass is ShapeLike
@@ -173,39 +320,28 @@ def __instancecheck__(cls, instance):
173320
class ShapeLike(metaclass=_ShapeLikeMeta):
174321
"""An abstract class representing all objects that can be cast to a :class:`Shape`.
175322
176-
``issubclass(cls, ShapeLike)`` returns ``True`` for:
323+
:pc:`issubclass(cls, ShapeLike)` returns :pc:`True` for:
177324
178-
- :class:`Shape`
179-
- :class:`ShapeCastable` and its subclasses
180-
- ``int`` and its subclasses
181-
- ``range`` and its subclasses
182-
- :class:`enum.EnumMeta` and its subclasses
183-
- :class:`ShapeLike` itself
325+
* :class:`Shape`;
326+
* :class:`ShapeCastable` and its subclasses;
327+
* :class:`int` and its subclasses;
328+
* :class:`range` and its subclasses;
329+
* :class:`enum.EnumMeta` and its subclasses;
330+
* :class:`ShapeLike` itself.
184331
185-
``isinstance(obj, ShapeLike)`` returns ``True`` for:
332+
:pc:`isinstance(obj, ShapeLike)` returns :pc:`True` for:
186333
187-
- :class:`Shape` instances
188-
- :class:`ShapeCastable` instances
189-
- non-negative ``int`` values
190-
- ``range`` instances
191-
- :class:`enum.Enum` subclasses where all values are :ref:`value-like <lang-valuelike>`
334+
* :class:`Shape` instances;
335+
* :class:`ShapeCastable` instances;
336+
* non-negative :class:`int` values;
337+
* :class:`range` instances;
338+
* :class:`enum.Enum` subclasses where all values are :ref:`value-like objects <lang-valuelike>`.
192339
193-
This class is only usable for the above checks — no instances and no (non-virtual)
194-
subclasses can be created.
340+
This class cannot be instantiated or subclassed. It can only be used for checking types of
341+
objects.
195342
"""
196-
197343
def __new__(cls, *args, **kwargs):
198-
raise TypeError("ShapeLike is an abstract class and cannot be constructed")
199-
200-
201-
def unsigned(width):
202-
"""Shorthand for ``Shape(width, signed=False)``."""
203-
return Shape(width, signed=False)
204-
205-
206-
def signed(width):
207-
"""Shorthand for ``Shape(width, signed=True)``."""
208-
return Shape(width, signed=True)
344+
raise TypeError("ShapeLike is an abstract class and cannot be instantiated")
209345

210346

211347
def _overridable_by_reflected(method_name):

0 commit comments

Comments
 (0)