lib.data: add reference documentation.

This commit is contained in:
Catherine 2023-02-20 21:19:46 +00:00
parent a5ffa38e64
commit 68e292c681
10 changed files with 792 additions and 10 deletions

View file

@ -12,6 +12,20 @@ __all__ = [
class Field:
"""Description of a data field.
The :class:`Field` class specifies the signedness and bit positions of a field in
an Amaranth value.
:class:`Field` objects are immutable.
Attributes
----------
shape : :ref:`shape-castable <lang-shapecasting>`
Shape of the field. When initialized or assigned, the object is stored as-is.
offset : :class:`int`, >=0
Index of the least significant bit of the field.
"""
def __init__(self, shape, offset):
try:
Shape.cast(shape)
@ -34,9 +48,24 @@ class Field:
@property
def width(self):
"""Width of the field.
This property should be used over ``self.shape.width`` because ``self.shape`` can be
an arbitrary :ref:`shape-castable <lang-shapecasting>` object, which may not have
a ``width`` property.
Returns
-------
:class:`int`
``Shape.cast(self.shape).width``
"""
return Shape.cast(self.shape).width
def __eq__(self, other):
"""Compare fields.
Two fields are equal if they have the same shape and offset.
"""
return (isinstance(other, Field) and
Shape.cast(self._shape) == Shape.cast(other.shape) and
self._offset == other.offset)
@ -46,9 +75,31 @@ class Field:
class Layout(ShapeCastable, metaclass=ABCMeta):
"""Description of a data layout.
The :ref:`shape-castable <lang-shapecasting>` :class:`Layout` interface associates keys
(string names or integer indexes) with fields, giving identifiers to spans of bits in
an Amaranth value.
It is an abstract base class; :class:`StructLayout`, :class:`UnionLayout`,
:class:`ArrayLayout`, and :class:`FlexibleLayout` implement concrete layout rules.
New layout rules can be defined by inheriting from this class.
"""
@staticmethod
def cast(obj):
"""Cast a shape-castable object to a layout."""
"""Cast a :ref:`shape-castable <lang-shapecasting>` object to a layout.
This method performs a subset of the operations done by :meth:`Shape.cast`; it will
recursively call ``.as_shape()``, but only until a layout is returned.
Raises
------
TypeError
If ``obj`` cannot be converted to a :class:`Layout` instance.
RecursionError
If ``obj.as_shape()`` returns ``obj``.
"""
while isinstance(obj, ShapeCastable):
if isinstance(obj, Layout):
return obj
@ -62,7 +113,13 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
@staticmethod
def of(obj):
"""Extract the layout from a view."""
"""Extract the layout that was used to create a view.
Raises
------
TypeError
If ``obj`` is not a :class:`View` instance.
"""
if not isinstance(obj, View):
raise TypeError("Object {!r} is not a data view"
.format(obj))
@ -70,21 +127,54 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
@abstractmethod
def __iter__(self):
"""Iterate the layout, yielding ``(key, field)`` pairs. Keys may be strings or integers."""
"""Iterate fields in the layout.
Yields
------
:class:`str` or :class:`int`
Key (either name or index) for accessing the field.
:class:`Field`
Description of the field.
"""
@abstractmethod
def __getitem__(self, key):
"""Retrieve the :class:`Field` associated with the ``key``, or raise ``KeyError``."""
"""Retrieve a field from the layout.
size = abstractproperty()
"""The number of bits in the representation defined by the layout."""
Returns
-------
:class:`Field`
The field associated with ``key``.
Raises
------
KeyError
If there is no field associated with ``key``.
"""
@property
@abstractmethod
def size(self):
"""Size of the layout.
Returns
-------
:class:`int`
The amount of bits required to store every field in the layout.
"""
def as_shape(self):
"""Convert the representation defined by the layout to an unsigned :class:`Shape`."""
"""Shape of the layout.
Returns
-------
:class:`Shape`
``unsigned(self.size)``
"""
return unsigned(self.size)
def __eq__(self, other):
"""Compare the layout with another.
"""Compare layouts.
Two layouts are equal if they have the same size and the same fields under the same names.
The order of the fields is not considered.
@ -123,6 +213,37 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
class StructLayout(Layout):
"""Description of a structure layout.
The fields of a structure layout follow one another without any gaps, and the size of
a structure layout is the sum of the sizes of its members.
For example, the following layout of a 16-bit value:
.. image:: _images/data/struct_layout.svg
can be described with:
.. testcode::
data.StructLayout({
"first": 3,
"second": 7,
"third": 6
})
.. note::
Structures that have padding can be described with a :class:`FlexibleLayout`. Alternately,
padding can be added to the layout as fields called ``_1``, ``_2``, and so on. These fields
won't be accessible as attributes or by using indexing.
Attributes
----------
members : mapping of :class:`str` to :ref:`shape-castable <lang-shapecasting>`
Dictionary of structure members.
"""
def __init__(self, members):
self.members = members
@ -158,6 +279,14 @@ class StructLayout(Layout):
@property
def size(self):
"""Size of the structure layout.
Returns
-------
int
Index of the most significant bit of the *last* field plus one; or zero if there are
no fields.
"""
return max((field.offset + field.width for field in self._fields.values()), default=0)
def __repr__(self):
@ -165,6 +294,30 @@ class StructLayout(Layout):
class UnionLayout(Layout):
"""Description of a union layout.
The fields of a union layout all start from bit 0, and the size of a union layout is the size
of the largest of its members.
For example, the following layout of a 7-bit value:
.. image:: _images/data/union_layout.svg
can be described with:
.. testcode::
data.UnionLayout({
"first": 3,
"second": 7,
"third": 6
})
Attributes
----------
members : mapping of :class:`str` to :ref:`shape-castable <lang-shapecasting>`
Dictionary of union members.
"""
def __init__(self, members):
self.members = members
@ -198,6 +351,14 @@ class UnionLayout(Layout):
@property
def size(self):
"""Size of the union layout.
Returns
-------
int
Index of the most significant bit of the *largest* field plus one; or zero if there are
no fields.
"""
return max((field.width for field in self._fields.values()), default=0)
def __repr__(self):
@ -205,6 +366,32 @@ class UnionLayout(Layout):
class ArrayLayout(Layout):
"""Description of an array layout.
The fields of an array layout follow one another without any gaps, and the size of an array
layout is the size of its element multiplied by its length.
For example, the following layout of a 16-bit value:
.. image:: _images/data/array_layout.svg
can be described with::
.. testcode::
data.ArrayLayout(unsigned(4), 4)
.. note::
Arrays that have padding can be described with a :class:`FlexibleLayout`.
Attributes
----------
elem_shape : :ref:`shape-castable <lang-shapecasting>`
Shape of an individual element.
length : int
Amount of elements.
"""
def __init__(self, elem_shape, length):
self.elem_shape = elem_shape
self.length = length
@ -252,6 +439,13 @@ class ArrayLayout(Layout):
@property
def size(self):
"""Size of the array layout.
Returns
-------
int
Size of an individual element multiplied by their amount.
"""
return Shape.cast(self._elem_shape).width * self.length
def __repr__(self):
@ -259,12 +453,42 @@ class ArrayLayout(Layout):
class FlexibleLayout(Layout):
"""Description of a flexible layout.
The fields of a flexible layout can be located arbitrarily, and its size is explicitly defined.
For example, the following layout of a 16-bit value:
.. image:: _images/data/flexible_layout.svg
can be described with:
.. testcode::
data.FlexibleLayout(16, {
"first": data.Field(unsigned(3), 1),
"second": data.Field(unsigned(7), 0),
"third": data.Field(unsigned(6), 10),
0: data.Field(unsigned(1), 14)
})
Both strings and integers can be used as names of flexible layout fields, so flexible layouts
can be used to describe structures and arrays with arbitrary padding.
Attributes
----------
size : int
Size of the layout.
fields : mapping of :class:`str` or :class:`int` to :class:`Field`
Fields defined in the layout.
"""
def __init__(self, size, fields):
self.size = size
self.fields = fields
@property
def size(self):
""":meta private:""" # work around Sphinx bug
return self._size
@size.setter
@ -319,6 +543,41 @@ class FlexibleLayout(Layout):
class View(ValueCastable):
"""A value viewed through the lens of a layout.
The :ref:`value-castable <lang-valuecasting>` class :class:`View` provides access to the fields
of an underlying Amaranth value via the names or indexes defined in the provided layout.
Creating a view
###############
When creating a view, either only the ``target`` argument, or any of the ``name``, ``reset``,
``reset_less``, ``attrs``, or ``decoder`` arguments may be provided. If a target is provided,
it is used as the underlying value. Otherwise, a new :class:`Signal` is created, and the rest
of the arguments are passed to its constructor.
Accessing a view
################
Slicing a view or accessing its attributes returns a part of the underlying value
corresponding to the field with that index or name, which is always an Amaranth value, but
it could also be a :class:`View` if the shape of the field is a :class:`Layout`, or
an instance of the data class if the shape of the field is a class deriving from
:class:`Struct` or :class:`Union`.
Slicing a view whose layout is an :class:`ArrayLayout` can be done with an index that is
an Amaranth value instead of a constant integer. The returned element is chosen dynamically
in that case.
Custom view classes
###################
The :class:`View` class can be inherited from to define additional properties or methods on
a view. The only two names that are reserved on instances of :class:`View` are :meth:`as_value`
and :meth:`eq`, leaving the rest to the developer. The :class:`Struct` and :class:`Union`
classes provided in this module are subclasses of :class:`View` that also provide a concise way
to define a layout.
"""
def __init__(self, layout, target=None, *, name=None, reset=None, reset_less=None,
attrs=None, decoder=None, src_loc_at=0):
try:
@ -356,12 +615,52 @@ class View(ValueCastable):
@ValueCastable.lowermethod
def as_value(self):
"""Get underlying value.
Returns
-------
:class:`Value`
The ``target`` provided when constructing the view, or the :class:`Signal` that
was created.
"""
return self.__target
def eq(self, other):
"""Assign to the underlying value.
Returns
-------
:class:`Assign`
``self.as_value().eq(other)``
"""
return self.as_value().eq(other)
def __getitem__(self, key):
"""Slice the underlying value.
A field corresponding to ``key`` is looked up in the layout. If the field's shape is
a :class:`Layout`, returns a :class:`View`. If it is a subclass of :class:`Struct` or
:class:`Union`, returns an instance of that class. Otherwise, returns an unspecified
Amaranth expression with the right shape.
Arguments
---------
key : :class:`str` or :class:`int` or :class:`ValueCastable`
Name or index of a field.
Returns
-------
:class:`Value`, inout
A slice of the underlying value defined by the field.
Raises
------
KeyError
If the layout does not define a field corresponding to ``key``.
TypeError
If ``key`` is a value-castable object, but the layout of the view is not
a :class:`ArrayLayout`.
"""
if isinstance(self.__layout, ArrayLayout):
shape = self.__layout.elem_shape
value = self.__target.word_select(key, Shape.cast(self.__layout.elem_shape).width)
@ -383,6 +682,16 @@ class View(ValueCastable):
return value
def __getattr__(self, name):
"""Access a field of the underlying value.
Returns ``self[name]``.
Raises
------
AttributeError
If the layout does not define a field called ``name``, or if ``name`` starts with
an underscore.
"""
try:
item = self[name]
except KeyError:
@ -459,8 +768,133 @@ class _Aggregate(View, metaclass=_AggregateMeta):
class Struct(_Aggregate):
"""Structures defined with annotations.
The :class:`Struct` base class is a subclass of :class:`View` that provides a concise way
to describe the structure layout and reset values for the fields using Python
:term:`variable annotations <python:variable annotation>`.
Any annotations containing :ref:`shape-castable <lang-shapecasting>` objects are used,
in the order in which they appear in the source code, to construct a :class:`StructLayout`.
The values assigned to such annotations are used to populate the reset value of the signal
created by the view. Any other annotations are kept as-is.
.. testsetup::
from amaranth import *
from amaranth.lib.data import *
As an example, a structure for `IEEE 754 single-precision floating-point format
<https://en.wikipedia.org/wiki/Single-precision_floating-point_format>`_ can be defined as:
.. testcode::
class IEEE754Single(Struct):
fraction: 23
exponent: 8 = 0x7f
sign: 1
def is_subnormal(self):
return self.exponent == 0
The ``IEEE754Single`` class itself can be used where a :ref:`shape <lang-shapes>` is expected:
.. doctest::
>>> IEEE754Single.as_shape()
StructLayout({'fraction': 23, 'exponent': 8, 'sign': 1})
>>> Signal(IEEE754Single).width
32
Instances of this class can be used where :ref:`values <lang-values>` are expected:
.. doctest::
>>> flt = IEEE754Single()
>>> Signal(32).eq(flt)
(eq (sig $signal) (sig flt))
Accessing shape-castable properties returns slices of the underlying value:
.. doctest::
>>> flt.fraction
(slice (sig flt) 0:23)
>>> flt.is_subnormal()
(== (slice (sig flt) 23:31) (const 1'd0))
The reset values for individual fields can be overridden during instantiation:
.. doctest::
>>> hex(IEEE754Single().as_value().reset)
'0x3f800000'
>>> hex(IEEE754Single(reset={'sign': 1}).as_value().reset)
'0xbf800000'
>>> hex(IEEE754Single(reset={'exponent': 0}).as_value().reset)
'0x0'
Classes inheriting from :class:`Struct` can be used as base classes. The only restrictions
are that:
* Classes that do not define a layout cannot be instantiated or converted to a shape;
* A layout can be defined exactly once in the inheritance hierarchy.
Behavior can be shared through inheritance:
.. testcode::
class HasChecksum(Struct):
def checksum(self):
bits = Value.cast(self)
return sum(bits[n:n+8] for n in range(0, len(bits), 8))
class BareHeader(HasChecksum):
address: 16
length: 8
class HeaderWithParam(HasChecksum):
address: 16
length: 8
param: 8
.. doctest::
>>> HasChecksum.as_shape()
Traceback (most recent call last):
...
TypeError: Aggregate class 'HasChecksum' does not have a defined shape
>>> bare = BareHeader(); bare.checksum()
(+ (+ (+ (const 1'd0) (slice (sig bare) 0:8)) (slice (sig bare) 8:16)) (slice (sig bare) 16:24))
>>> param = HeaderWithParam(); param.checksum()
(+ (+ (+ (+ (const 1'd0) (slice (sig param) 0:8)) (slice (sig param) 8:16)) (slice (sig param) 16:24)) (slice (sig param) 24:32))
"""
_AggregateMeta__layout_cls = StructLayout
class Union(_Aggregate):
"""Unions defined with annotations.
The :class:`Union` base class is a subclass of :class:`View` that provides a concise way
to describe the union layout using Python :term:`variable annotations <python:variable
annotation>`. It is very similar to the :class:`Struct` class, except that its layout
is a :class:`UnionLayout`.
A :class:`Union` can have only one field with a specified reset value. If a reset value is
explicitly provided during instantiation, it overrides the reset value specified with
an annotation:
.. testcode::
class VarInt(Union):
int8: 8
int16: 16 = 0x100
.. doctest::
>>> VarInt().as_value().reset
256
>>> VarInt(reset={'int8': 10}).as_value().reset
10
"""
_AggregateMeta__layout_cls = UnionLayout