Skip to content

Commit ea3d6c9

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 c9b87a4 commit ea3d6c9

File tree

6 files changed

+325
-79
lines changed

6 files changed

+325
-79
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/_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)