amaranth/amaranth/lib/data.py

467 lines
18 KiB
Python

from abc import ABCMeta, abstractmethod, abstractproperty
from collections.abc import Mapping, Sequence
from amaranth.hdl import *
from amaranth.hdl.ast import ShapeCastable, ValueCastable
__all__ = [
"Field", "Layout", "StructLayout", "UnionLayout", "ArrayLayout", "FlexibleLayout",
"View", "Struct", "Union",
]
class Field:
def __init__(self, shape, offset):
try:
Shape.cast(shape)
except TypeError as e:
raise TypeError("Field shape must be a shape-castable object, not {!r}"
.format(shape)) from e
if not isinstance(offset, int) or offset < 0:
raise TypeError("Field offset must be a non-negative integer, not {!r}"
.format(offset))
self._shape = shape
self._offset = offset
@property
def shape(self):
return self._shape
@property
def offset(self):
return self._offset
@property
def width(self):
return Shape.cast(self.shape).width
def __eq__(self, other):
return (isinstance(other, Field) and
Shape.cast(self._shape) == Shape.cast(other.shape) and
self._offset == other.offset)
def __repr__(self):
return f"Field({self._shape!r}, {self._offset})"
class Layout(ShapeCastable, metaclass=ABCMeta):
@staticmethod
def cast(obj):
"""Cast a shape-castable object to a layout."""
while isinstance(obj, ShapeCastable):
if isinstance(obj, Layout):
return obj
new_obj = obj.as_shape()
if new_obj is obj:
break
obj = new_obj
Shape.cast(obj) # delegate non-layout-specific error handling to Shape
raise TypeError("Object {!r} cannot be converted to a data layout"
.format(obj))
@staticmethod
def of(obj):
"""Extract the layout from a view."""
if not isinstance(obj, View):
raise TypeError("Object {!r} is not a data view"
.format(obj))
return obj._View__orig_layout
@abstractmethod
def __iter__(self):
"""Iterate the layout, yielding ``(key, field)`` pairs. Keys may be strings or integers."""
@abstractmethod
def __getitem__(self, key):
"""Retrieve the :class:`Field` associated with the ``key``, or raise ``KeyError``."""
size = abstractproperty()
"""The number of bits in the representation defined by the layout."""
def as_shape(self):
"""Convert the representation defined by the layout to an unsigned :class:`Shape`."""
return unsigned(self.size)
def __eq__(self, other):
"""Compare the layout with another.
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.
"""
while isinstance(other, ShapeCastable) and not isinstance(other, Layout):
new_other = other.as_shape()
if new_other is other:
break
other = new_other
return (isinstance(other, Layout) and self.size == other.size and
dict(iter(self)) == dict(iter(other)))
def _convert_to_int(self, value):
"""Convert ``value``, which may be a dict or an array of field values, to an integer using
the representation defined by this layout.
This method is private because Amaranth does not currently have a concept of
a constant initializer; this requires an RFC. It will be renamed or removed
in a future version.
"""
if isinstance(value, Mapping):
iterator = value.items()
elif isinstance(value, Sequence):
iterator = enumerate(value)
else:
raise TypeError("Layout initializer must be a mapping or a sequence, not {!r}"
.format(value))
int_value = 0
for key, key_value in iterator:
field = self[key]
if isinstance(field.shape, Layout):
key_value = field.shape._convert_to_int(key_value)
int_value |= Const(key_value, Shape.cast(field.shape)).value << field.offset
return int_value
class StructLayout(Layout):
def __init__(self, members):
self.members = members
@property
def members(self):
return {key: field.shape for key, field in self._fields.items()}
@members.setter
def members(self, members):
offset = 0
self._fields = {}
if not isinstance(members, Mapping):
raise TypeError("Struct layout members must be provided as a mapping, not {!r}"
.format(members))
for key, shape in members.items():
if not isinstance(key, str):
raise TypeError("Struct layout member name must be a string, not {!r}"
.format(key))
try:
cast_shape = Shape.cast(shape)
except TypeError as e:
raise TypeError("Struct layout member shape must be a shape-castable object, "
"not {!r}"
.format(shape)) from e
self._fields[key] = Field(shape, offset)
offset += cast_shape.width
def __iter__(self):
return iter(self._fields.items())
def __getitem__(self, key):
return self._fields[key]
@property
def size(self):
return max((field.offset + field.width for field in self._fields.values()), default=0)
def __repr__(self):
return f"StructLayout({self.members!r})"
class UnionLayout(Layout):
def __init__(self, members):
self.members = members
@property
def members(self):
return {key: field.shape for key, field in self._fields.items()}
@members.setter
def members(self, members):
self._fields = {}
if not isinstance(members, Mapping):
raise TypeError("Union layout members must be provided as a mapping, not {!r}"
.format(members))
for key, shape in members.items():
if not isinstance(key, str):
raise TypeError("Union layout member name must be a string, not {!r}"
.format(key))
try:
cast_shape = Shape.cast(shape)
except TypeError as e:
raise TypeError("Union layout member shape must be a shape-castable object, "
"not {!r}"
.format(shape)) from e
self._fields[key] = Field(shape, 0)
def __iter__(self):
return iter(self._fields.items())
def __getitem__(self, key):
return self._fields[key]
@property
def size(self):
return max((field.width for field in self._fields.values()), default=0)
def __repr__(self):
return f"UnionLayout({self.members!r})"
class ArrayLayout(Layout):
def __init__(self, elem_shape, length):
self.elem_shape = elem_shape
self.length = length
@property
def elem_shape(self):
return self._elem_shape
@elem_shape.setter
def elem_shape(self, elem_shape):
try:
Shape.cast(elem_shape)
except TypeError as e:
raise TypeError("Array layout element shape must be a shape-castable object, "
"not {!r}"
.format(elem_shape)) from e
self._elem_shape = elem_shape
@property
def length(self):
return self._length
@length.setter
def length(self, length):
if not isinstance(length, int) or length < 0:
raise TypeError("Array layout length must be a non-negative integer, not {!r}"
.format(length))
self._length = length
def __iter__(self):
offset = 0
for index in range(self._length):
yield index, Field(self._elem_shape, offset)
offset += Shape.cast(self._elem_shape).width
def __getitem__(self, key):
if isinstance(key, int):
if key not in range(-self._length, self._length):
# Layout's interface requires us to raise KeyError, not IndexError
raise KeyError(key)
if key < 0:
key += self._length
return Field(self._elem_shape, key * Shape.cast(self._elem_shape).width)
raise TypeError("Cannot index array layout with {!r}".format(key))
@property
def size(self):
return Shape.cast(self._elem_shape).width * self.length
def __repr__(self):
return f"ArrayLayout({self._elem_shape!r}, {self.length})"
class FlexibleLayout(Layout):
def __init__(self, size, fields):
self.size = size
self.fields = fields
@property
def size(self):
return self._size
@size.setter
def size(self, size):
if not isinstance(size, int) or size < 0:
raise TypeError("Flexible layout size must be a non-negative integer, not {!r}"
.format(size))
if hasattr(self, "_fields") and self._fields:
endmost_name, endmost_field = max(self._fields.items(),
key=lambda pair: pair[1].offset + pair[1].width)
if endmost_field.offset + endmost_field.width > size:
raise ValueError("Flexible layout size {} does not cover the field '{}', which "
"ends at bit {}"
.format(size, endmost_name,
endmost_field.offset + endmost_field.width))
self._size = size
@property
def fields(self):
return {**self._fields}
@fields.setter
def fields(self, fields):
self._fields = {}
if not isinstance(fields, Mapping):
raise TypeError("Flexible layout fields must be provided as a mapping, not {!r}"
.format(fields))
for key, field in fields.items():
if not isinstance(key, (int, str)) or (isinstance(key, int) and key < 0):
raise TypeError("Flexible layout field name must be a non-negative integer or "
"a string, not {!r}"
.format(key))
if not isinstance(field, Field):
raise TypeError("Flexible layout field value must be a Field instance, not {!r}"
.format(field))
if field.offset + field.width > self._size:
raise ValueError("Flexible layout field '{}' ends at bit {}, exceeding "
"the size of {} bit(s)"
.format(key, field.offset + field.width, self._size))
self._fields[key] = field
def __iter__(self):
return iter(self._fields.items())
def __getitem__(self, key):
if isinstance(key, (int, str)):
return self._fields[key]
raise TypeError("Cannot index flexible layout with {!r}".format(key))
def __repr__(self):
return f"FlexibleLayout({self._size}, {self._fields!r})"
class View(ValueCastable):
def __init__(self, layout, target=None, *, name=None, reset=None, reset_less=None,
attrs=None, decoder=None, src_loc_at=0):
try:
cast_layout = Layout.cast(layout)
except TypeError as e:
raise TypeError("View layout must be a layout, not {!r}"
.format(layout)) from e
if target is not None:
if (name is not None or reset is not None or reset_less is not None or
attrs is not None or decoder is not None):
raise ValueError("View target cannot be provided at the same time as any of "
"the Signal constructor arguments (name, reset, reset_less, "
"attrs, decoder)")
try:
cast_target = Value.cast(target)
except TypeError as e:
raise TypeError("View target must be a value-castable object, not {!r}"
.format(target)) from e
if len(cast_target) != cast_layout.size:
raise ValueError("View target is {} bit(s) wide, which is not compatible with "
"the {} bit(s) wide view layout"
.format(len(cast_target), cast_layout.size))
else:
if reset is None:
reset = 0
else:
reset = cast_layout._convert_to_int(reset)
if reset_less is None:
reset_less = False
cast_target = Signal(cast_layout, name=name, reset=reset, reset_less=reset_less,
attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
self.__orig_layout = layout
self.__layout = cast_layout
self.__target = cast_target
@ValueCastable.lowermethod
def as_value(self):
return self.__target
def eq(self, other):
return self.as_value().eq(other)
def __getitem__(self, key):
if isinstance(self.__layout, ArrayLayout):
shape = self.__layout.elem_shape
value = self.__target.word_select(key, Shape.cast(self.__layout.elem_shape).width)
else:
if isinstance(key, (Value, ValueCastable)):
raise TypeError("Only views with array layout, not {!r}, may be indexed "
"with a value"
.format(self.__layout))
field = self.__layout[key]
shape = field.shape
value = self.__target[field.offset:field.offset + field.width]
if isinstance(shape, _AggregateMeta):
return shape(value)
if isinstance(shape, Layout):
return View(shape, value)
if Shape.cast(shape).signed:
return value.as_signed()
else:
return value
def __getattr__(self, name):
try:
item = self[name]
except KeyError:
raise AttributeError("View of {!r} does not have a field {!r}; "
"did you mean one of: {}?"
.format(self.__target, name,
", ".join(repr(name)
for name, field in self.__layout)))
if name.startswith("_"):
raise AttributeError("View of {!r} field {!r} has a reserved name and may only be "
"accessed by indexing"
.format(self.__target, name))
return item
class _AggregateMeta(ShapeCastable, type):
def __new__(metacls, name, bases, namespace):
if "__annotations__" not in namespace:
# This is a base class without its own layout. It is not shape-castable, and cannot
# be instantiated. It can be used to share behavior.
return type.__new__(metacls, name, bases, namespace)
elif all(not hasattr(base, "_AggregateMeta__layout") for base in bases):
# This is a leaf class with its own layout. It is shape-castable and can
# be instantiated. It can also be subclassed, and used to share layout and behavior.
layout = dict()
reset = dict()
for name in {**namespace["__annotations__"]}:
try:
Shape.cast(namespace["__annotations__"][name])
except TypeError:
# Not a shape-castable annotation; leave as-is.
continue
layout[name] = namespace["__annotations__"].pop(name)
if name in namespace:
reset[name] = namespace.pop(name)
cls = type.__new__(metacls, name, bases, namespace)
if cls.__layout_cls is UnionLayout:
if len(reset) > 1:
raise ValueError("Reset value for at most one field can be provided for "
"a union class (specified: {})"
.format(", ".join(reset.keys())))
cls.__layout = cls.__layout_cls(layout)
cls.__reset = reset
return cls
else:
# This is a class that has a base class with a layout and annotations. Such a class
# is not well-formed.
raise TypeError("Aggregate class '{}' must either inherit or specify a layout, "
"not both"
.format(name))
def as_shape(cls):
if not hasattr(cls, "_AggregateMeta__layout"):
raise TypeError("Aggregate class '{}.{}' does not have a defined shape"
.format(cls.__module__, cls.__qualname__))
return cls.__layout
class _Aggregate(View, metaclass=_AggregateMeta):
def __init__(self, target=None, *, name=None, reset=None, reset_less=None,
attrs=None, decoder=None, src_loc_at=0):
if self.__class__._AggregateMeta__layout_cls is UnionLayout:
if reset is not None and len(reset) > 1:
raise ValueError("Reset value for at most one field can be provided for "
"a union class (specified: {})"
.format(", ".join(reset.keys())))
if target is None and hasattr(self.__class__, "_AggregateMeta__reset"):
if reset is None:
reset = self.__class__._AggregateMeta__reset
elif self.__class__._AggregateMeta__layout_cls is not UnionLayout:
reset = {**self.__class__._AggregateMeta__reset, **reset}
super().__init__(self.__class__, target, name=name, reset=reset, reset_less=reset_less,
attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
class Struct(_Aggregate):
_AggregateMeta__layout_cls = StructLayout
class Union(_Aggregate):
_AggregateMeta__layout_cls = UnionLayout