lib.data: add reference documentation.
This commit is contained in:
parent
a5ffa38e64
commit
68e292c681
10 changed files with 792 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue