Implement RFC 51: Add ShapeCastable.from_bits and amaranth.lib.data.Const.

Co-authored-by: Catherine <whitequark@whitequark.org>
This commit is contained in:
Wanda 2024-03-14 20:07:53 +01:00 committed by Catherine
parent 598cf8db28
commit d6bf47d549
8 changed files with 705 additions and 109 deletions

View file

@ -243,6 +243,10 @@ class ShapeCastable:
if cls.__call__ is ShapeCastable.__call__:
raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
f"the '__call__' method")
if cls.from_bits is ShapeCastable.from_bits:
warnings.warn(f"Class '{cls.__name__}' deriving from 'ShapeCastable' does not override "
f"the 'from_bits' method, which will be required in Amaranth 0.6",
DeprecationWarning, stacklevel=2)
# The signatures and definitions of these methods are weird because they are present here for
# documentation (and error checking above) purpose only and should not affect control flow.
@ -308,6 +312,40 @@ class ShapeCastable:
"""
return super().const(*args, **kwargs) # :nocov:
def from_bits(self, raw):
"""Lift a bit pattern to a higher-level representation.
This method is called by the Amaranth language to lift :py:`raw`, which is an :class:`int`,
to a higher-level representation, which may be any object accepted by :meth:`const`.
Most importantly, the simulator calls this method when the value of a shape-castable
object is retrieved.
For any valid bit pattern :py:`raw`, the following condition must hold:
.. code::
Const.cast(self.const(self.from_bits(raw))).value == raw
While :meth:`const` will usually return an Amaranth value or a custom value-castable
object that is convenient to use while constructing the design, this method will usually
return a Python object that is convenient to use while simulating the design. While not
constrained here, these objects should have the same type whenever feasible.
This method may also be called by code that is not a part of the Amaranth language.
Returns
-------
unspecified type
Raises
------
Exception
When the bit pattern isn't valid. This exception must be propagated by callers,
either directly or as a cause of another exception. While not constrained here,
usually the exception class will be :exc:`ValueError`.
"""
return raw
def __call__(self, *args, **kwargs):
"""__call__(obj)

View file

@ -2,16 +2,17 @@ from abc import ABCMeta, abstractmethod
from enum import Enum
from collections.abc import Mapping, Sequence
import warnings
import operator
from amaranth._utils import final
from amaranth.hdl import *
from amaranth.hdl._repr import *
from amaranth.hdl._ast import ShapeCastable, ValueCastable
from amaranth import hdl
__all__ = [
"Field", "Layout", "StructLayout", "UnionLayout", "ArrayLayout", "FlexibleLayout",
"View", "Struct", "Union",
"View", "Const", "Struct", "Union",
]
@ -26,7 +27,7 @@ class Field:
Attributes
----------
shape : :ref:`shape-like <lang-shapelike>`
shape : :class:`.ShapeLike`
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.
@ -55,14 +56,14 @@ class Field:
def width(self):
"""Width of the field.
This property should be used over ``self.shape.width`` because ``self.shape`` can be
This property should be used over :py:`self.shape.width` because :py:`self.shape` can be
an arbitrary :ref:`shape-like <lang-shapelike>` object, which may not have
a ``width`` property.
a :py:`width` property.
Returns
-------
:class:`int`
``Shape.cast(self.shape).width``
:py:`Shape.cast(self.shape).width`
"""
return Shape.cast(self.shape).width
@ -98,15 +99,15 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
def cast(obj):
"""Cast a :ref:`shape-like <lang-shapelike>` 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.
This method performs a subset of the operations done by :meth:`.Shape.cast`; it will
recursively call :py:`.as_shape()`, but only until a layout is returned.
Raises
------
TypeError
If ``obj`` cannot be converted to a :class:`Layout` instance.
If :py:`obj` cannot be converted to a :class:`Layout` instance.
RecursionError
If ``obj.as_shape()`` returns ``obj``.
If :py:`obj.as_shape()` returns :py:`obj`.
"""
while isinstance(obj, ShapeCastable):
if isinstance(obj, Layout):
@ -138,12 +139,12 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
Returns
-------
:class:`Field`
The field associated with ``key``.
The field associated with :py:`key`.
Raises
------
KeyError
If there is no field associated with ``key``.
If there is no field associated with :py:`key`.
"""
@property
@ -162,8 +163,8 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
Returns
-------
:class:`Shape`
``unsigned(self.size)``
:class:`.Shape`
:py:`unsigned(self.size)`
"""
return unsigned(self.size)
@ -191,21 +192,21 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
Returns
-------
:class:`View`
``View(self, target)``
:py:`View(self, target)`
"""
return View(self, target)
def const(self, init):
"""Convert a constant initializer to a constant.
Converts ``init``, which may be a sequence or a mapping of field values, to a constant.
Converts :py:`init`, which may be a sequence or a mapping of field values, to a constant.
Returns
-------
:class:`Const`
A constant that has the same value as a view with this layout that was initialized with
an all-zero value and had every field assigned to the corresponding value in the order
in which they appear in ``init``.
in which they appear in :py:`init`.
"""
if init is None:
iterator = iter(())
@ -222,18 +223,30 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
field = self[key]
cast_field_shape = Shape.cast(field.shape)
if isinstance(field.shape, ShapeCastable):
key_value = Const.cast(field.shape.const(key_value))
key_value = hdl.Const.cast(field.shape.const(key_value))
if key_value.shape() != cast_field_shape:
raise ValueError("Constant returned by {!r}.const() must have the shape that "
"it casts to, {!r}, and not {!r}"
.format(field.shape, cast_field_shape,
key_value.shape()))
elif not isinstance(key_value, Const):
key_value = Const(key_value, cast_field_shape)
elif not isinstance(key_value, hdl.Const):
key_value = hdl.Const(key_value, cast_field_shape)
mask = ((1 << cast_field_shape.width) - 1) << field.offset
int_value &= ~mask
int_value |= (key_value.value << field.offset) & mask
return View(self, Const(int_value, self.as_shape()))
return Const(self, int_value)
def from_bits(self, raw):
"""Convert a bit pattern to a constant.
Converts :py:`raw`, which is an :class:`int`, to a constant.
Returns
-------
:class:`Const`
:py:`Const(self, raw)`
"""
return Const(self, raw)
def _value_repr(self, value):
yield Repr(FormatInt(), value)
@ -279,7 +292,7 @@ class StructLayout(Layout):
Attributes
----------
members : mapping of :class:`str` to :ref:`shape-like <lang-shapelike>`
members : mapping of :class:`str` to :class:`.ShapeLike`
Dictionary of structure members.
"""
@ -350,7 +363,7 @@ class UnionLayout(Layout):
Attributes
----------
members : mapping of :class:`str` to :ref:`shape-like <lang-shapelike>`
members : mapping of :class:`str` to :class:`.ShapeLike`
Dictionary of union members.
"""
def __init__(self, members):
@ -425,7 +438,7 @@ class ArrayLayout(Layout):
Attributes
----------
elem_shape : :ref:`shape-like <lang-shapelike>`
elem_shape : :class:`.ShapeLike`
Shape of an individual element.
length : :class:`int`
Amount of elements.
@ -573,7 +586,10 @@ class View(ValueCastable):
Creating a view
###############
A view must be created using an explicitly provided layout and target. To create a new :class:`Signal` that is wrapped in a :class:`View` with a given ``layout``, use ``Signal(layout, ...)``, which for a :class:`Layout` is equivalent to ``View(layout, Signal(...))``.
A view must be created using an explicitly provided layout and target. To create a new
:class:`Signal` that is wrapped in a :class:`View` with a given :py:`layout`, use
:py:`Signal(layout, ...)`, which for a :class:`Layout` is equivalent to
:py:`View(layout, Signal(...))`.
Accessing a view
################
@ -582,46 +598,46 @@ class View(ValueCastable):
corresponding to the field with that index or name, which is itself either a value or
a value-castable object. If the shape of the field is a :class:`Layout`, it will be
a :class:`View`; if it is a class deriving from :class:`Struct` or :class:`Union`, it
will be an instance of that data class; if it is another
:ref:`shape-like <lang-shapelike>` object implementing ``__call__``, it will be
the result of calling that method.
will be an instance of that data class; if it is another :ref:`shape-like <lang-shapelike>`
object implementing :meth:`~.ShapeCastable.__call__`, it will be the result of calling that
method.
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
an Amaranth value rather than a constant integer. The returned element is chosen dynamically
in that case.
A view can only be compared for equality with another view of the same layout,
returning a single-bit value. No other operators are supported on views. If required,
a view can be converted back to its underlying value via :meth:`as_value`.
A view can only be compared for equality with another view or constant with the same layout,
returning a single-bit :class:`.Value`. No other operators are supported. A view can be
lowered to a :class:`.Value` using :meth:`as_value`.
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.
a view. The only three names that are reserved on instances of :class:`View` and :class:`Const`
are :meth:`as_value`, :meth:`Const.as_bits`, 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):
try:
cast_layout = Layout.cast(layout)
except TypeError as e:
raise TypeError("View layout must be a layout, not {!r}"
raise TypeError("Layout of a view must be a Layout, not {!r}"
.format(layout)) from e
try:
cast_target = Value.cast(target)
except TypeError as e:
raise TypeError("View target must be a value-castable object, not {!r}"
raise TypeError("Target of a view 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"
raise ValueError("Target of a view is {} bit(s) wide, which is not compatible with "
"its {} bit(s) wide layout"
.format(len(cast_target), cast_layout.size))
for name, field in cast_layout:
if isinstance(name, str) and name[0] != "_" and hasattr(type(self), name):
warnings.warn("View layout includes a field {!r} that will be shadowed by "
"the view attribute '{}.{}.{}'"
warnings.warn("Layout of a view includes a field {!r} that will be shadowed by "
"the attribute '{}.{}.{}'"
.format(name, type(self).__module__, type(self).__qualname__, name),
SyntaxWarning, stacklevel=2)
self.__orig_layout = layout
@ -634,7 +650,7 @@ class View(ValueCastable):
Returns
-------
:class:`Layout`
The ``layout`` provided when constructing the view.
The :py:`layout` provided when constructing the view.
"""
return self.__orig_layout
@ -643,8 +659,8 @@ class View(ValueCastable):
Returns
-------
:class:`Value`
The ``target`` provided when constructing the view, or the :class:`Signal` that
:class:`.Value`
The :py:`target` provided when constructing the view, or the :class:`Signal` that
was created.
"""
return self.__target
@ -654,52 +670,53 @@ class View(ValueCastable):
Returns
-------
:class:`Assign`
``self.as_value().eq(other)``
:class:`.Assign`
:py:`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 shape-castable object that has a ``__call__`` method, it is called and the result is
returned. Otherwise, ``as_shape`` is called repeatedly on the shape until either an object
with a ``__call__`` method is reached, or a ``Shape`` is returned. In the latter case,
returns an unspecified Amaranth expression with the right shape.
A field corresponding to :py:`key` is looked up in the layout. If the field's shape is
a shape-castable object that has a :meth:`~.ShapeCastable.__call__` method, it is called and
the result is returned. Otherwise, :meth:`~.ShapeCastable.as_shape` is called repeatedly on
the shape until either an object with a :meth:`~.ShapeCastable.__call__` method is reached,
or a :class:`.Shape` is returned. In the latter case, returns an unspecified Amaranth
expression with the right shape.
Arguments
---------
key : :class:`str` or :class:`int` or :class:`ValueCastable`
key : :class:`str` or :class:`int` or :class:`.ValueCastable`
Name or index of a field.
Returns
-------
:class:`Value` or :class:`ValueCastable`, inout
:class:`.Value` or :class:`.ValueCastable`, :ref:`assignable <lang-assignable>`
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`.
TypeError
If ``ShapeCastable.__call__`` does not return a value or a value-castable object.
:exc:`KeyError`
If the layout does not define a field corresponding to :py:`key`.
:exc:`TypeError`
If :py:`key` is a value-castable object, but the layout of the view is not
an :class:`ArrayLayout`.
:exc:`TypeError`
If :meth:`.ShapeCastable.__call__` does not return a value or a value-castable object.
"""
if isinstance(self.__layout, ArrayLayout):
if not isinstance(key, (int, Value, ValueCastable)):
raise TypeError("Views with array layout may only be indexed with an integer "
"or a value, not {!r}"
.format(key))
raise TypeError(
f"View with array layout may only be indexed with an integer or a value, "
f"not {key!r}")
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))
raise TypeError(
f"Only views with array layout, not {self.__layout!r}, may be indexed with "
f"a value")
field = self.__layout[key]
shape = field.shape
value = self.__target[field.offset:field.offset + field.width]
@ -708,9 +725,9 @@ class View(ValueCastable):
if isinstance(shape, ShapeCastable):
value = shape(value)
if not isinstance(value, (Value, ValueCastable)):
raise TypeError("{!r}.__call__() must return a value or "
"a value-castable object, not {!r}"
.format(shape, value))
raise TypeError(
f"{shape!r}.__call__() must return a value or a value-castable object, not "
f"{value!r}")
return value
if Shape.cast(shape).signed:
return value.as_signed()
@ -720,40 +737,48 @@ class View(ValueCastable):
def __getattr__(self, name):
"""Access a field of the underlying value.
Returns ``self[name]``.
Returns :py:`self[name]`.
Raises
------
AttributeError
If the layout does not define a field called ``name``, or if ``name`` starts with
:exc:`AttributeError`
If the layout does not define a field called :py:`name`, or if :py:`name` starts with
an underscore.
"""
if isinstance(self.__layout, ArrayLayout):
raise AttributeError("View of {!r} with an array layout does not have fields"
.format(self.__target))
raise AttributeError(
f"View with an array layout does not have fields")
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)))
raise AttributeError(
f"View with layout {self.__layout!r} does not have a field {name!r}; did you mean "
f"one of: {', '.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))
raise AttributeError(
f"Field {name!r} of view with layout {self.__layout!r} has a reserved name and "
f"may only be accessed by indexing")
return item
def __eq__(self, other):
if not isinstance(other, View) or self.__layout != other.__layout:
raise TypeError(f"View of {self.__layout!r} can only be compared to another view of the same layout, not {other!r}")
return self.__target == other.__target
if isinstance(other, View) and self.__layout == other.__layout:
return self.__target == other.__target
elif isinstance(other, Const) and self.__layout == other._Const__layout:
return self.__target == other.as_value()
else:
raise TypeError(
f"View with layout {self.__layout!r} can only be compared to another view or "
f"constant with the same layout, not {other!r}")
def __ne__(self, other):
if not isinstance(other, View) or self.__layout != other.__layout:
raise TypeError(f"View of {self.__layout!r} can only be compared to another view of the same layout, not {other!r}")
return self.__target != other.__target
if isinstance(other, View) and self.__layout == other.__layout:
return self.__target != other.__target
elif isinstance(other, Const) and self.__layout == other._Const__layout:
return self.__target != other.as_value()
else:
raise TypeError(
f"View with layout {self.__layout!r} can only be compared to another view or "
f"constant with the same layout, not {other!r}")
def __add__(self, other):
raise TypeError("Cannot perform arithmetic operations on a View")
@ -789,6 +814,223 @@ class View(ValueCastable):
return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})"
class Const(ValueCastable):
"""A constant value viewed through the lens of a layout.
The :class:`Const` class is similar to the :class:`View` class, except that its target is
a specific bit pattern and operations on it return constants.
Creating a constant
###################
A constant can be created from a :class:`dict` or :class:`list` of field values using
:meth:`Layout.const`, or from a bit pattern using :meth:`Layout.from_bits`.
Accessing a constant
####################
Slicing a constant or accessing its attributes returns a part of the underlying value
corresponding to the field with that index or name. If the shape of the field is
a :class:`Layout`, the returned value is a :class:`Const`; if it is a different
:ref:`shape-like <lang-shapelike>` object implementing :meth:`~.ShapeCastable.from_bits`,
it will be the result of calling that method; otherwise, it is an :class:`int`.
Slicing a constant whose layout is an :class:`ArrayLayout` can be done with an index that is
an Amaranth value rather than a constant integer. The returned element is chosen dynamically
in that case, and the resulting value will be a :class:`View` instead of a :class:`Const`.
A :class:`Const` can only be compared for equality with another constant or view that has
the same layout. When compared with another constant, the result will be a :class:`bool`.
When compared with a view, the result will be a single-bit :class:`.Value`. No other operators
are supported. A constant can be lowered to a :class:`.Value` using :meth:`as_value`, or to
its underlying bit pattern using :meth:`as_bits`.
"""
def __init__(self, layout, target):
try:
cast_layout = Layout.cast(layout)
except TypeError as e:
raise TypeError(f"Layout of a constant must be a Layout, not {layout!r}") from e
try:
target = operator.index(target)
except TypeError as e:
raise TypeError(f"Target of a constant must be an int, not {target!r}") from e
if target not in range(1 << cast_layout.size):
raise ValueError(f"Target of a constant does not fit in {cast_layout.size} bit(s)")
for name, field in cast_layout:
if isinstance(name, str) and name[0] != "_" and hasattr(type(self), name):
warnings.warn("Layout of a constant includes a field {!r} that will be shadowed by "
"the attribute '{}.{}.{}'"
.format(name, type(self).__module__, type(self).__qualname__, name),
SyntaxWarning, stacklevel=2)
self.__orig_layout = layout
self.__layout = cast_layout
self.__target = target
def shape(self):
"""Get layout of this constant.
Returns
-------
:class:`Layout`
The :py:`layout` provided when constructing the constant.
"""
return self.__orig_layout
def as_bits(self):
"""Get underlying bit pattern.
Returns
-------
:class:`int`
The :py:`target` provided when constructing the constant.
"""
return self.__target
def as_value(self):
"""Convert to a value.
Returns
-------
:class:`.Const`
The bit pattern of this constant, as a :class:`.Value`.
"""
return hdl.Const(self.__target, self.__layout.size)
def __getitem__(self, key):
"""Slice the underlying value.
A field corresponding to :py:`key` is looked up in the layout. If the field's shape is
a shape-castable object that has a :meth:`~.ShapeCastable.from_bits` method, returns
the result of calling that method. Otherwise, returns an :class:`int`.
Arguments
---------
key : :class:`str` or :class:`int` or :class:`.ValueCastable`
Name or index of a field.
Returns
-------
unspecified type or :class:`int`
A slice of the underlying value defined by the field.
Raises
------
:exc:`KeyError`
If the layout does not define a field corresponding to :py:`key`.
:exc:`TypeError`
If :py:`key` is a value-castable object, but the layout of the constant is not
an :class:`ArrayLayout`.
:exc:`Exception`
If the bit pattern of the field is not valid according to
:meth:`.ShapeCastable.from_bits`. Usually this will be a :exc:`ValueError`.
"""
if isinstance(self.__layout, ArrayLayout):
if isinstance(key, (Value, ValueCastable)):
return View(self.__layout, self.as_value())[key]
if not isinstance(key, int):
raise TypeError(
f"Constant with array layout may only be indexed with an integer or a value, "
f"not {key!r}")
shape = self.__layout.elem_shape
elem_width = Shape.cast(self.__layout.elem_shape).width
value = (self.__target >> key * elem_width) & ((1 << elem_width) - 1)
else:
if isinstance(key, (Value, ValueCastable)):
raise TypeError(
f"Only constants with array layout, not {self.__layout!r}, may be indexed with "
f"a value")
field = self.__layout[key]
shape = field.shape
value = (self.__target >> field.offset) & ((1 << field.width) - 1)
# Field guarantees that the shape-castable object is well-formed, so there is no need
# to handle erroneous cases here.
if isinstance(shape, ShapeCastable):
return shape.from_bits(value)
return hdl.Const(value, Shape.cast(shape)).value
def __getattr__(self, name):
"""Access a field of the underlying value.
Returns :py:`self[name]`.
Raises
------
:exc:`AttributeError`
If the layout does not define a field called :py:`name`, or if :py:`name` starts with
an underscore.
:exc:`Exception`
If the bit pattern of the field is not valid according to
:meth:`.ShapeCastable.from_bits`. Usually this will be a :exc:`ValueError`.
"""
if isinstance(self.__layout, ArrayLayout):
raise AttributeError(
f"Constant with an array layout does not have fields")
try:
item = self[name]
except KeyError:
raise AttributeError(
f"Constant with layout {self.__layout!r} does not have a field {name!r}; did you mean "
f"one of: {', '.join(repr(name) for name, field in self.__layout)}?")
if name.startswith("_"):
raise AttributeError(
f"Field {name!r} of constant with layout {self.__layout!r} has a reserved name and "
f"may only be accessed by indexing")
return item
def __eq__(self, other):
if isinstance(other, View) and self.__layout == other._View__layout:
return self.as_value() == other._View__target
elif isinstance(other, Const) and self.__layout == other.__layout:
return self.__target == other.__target
else:
raise TypeError(
f"Constant with layout {self.__layout!r} can only be compared to another view or "
f"constant with the same layout, not {other!r}")
def __ne__(self, other):
if isinstance(other, View) and self.__layout == other._View__layout:
return self.as_value() != other._View__target
elif isinstance(other, Const) and self.__layout == other.__layout:
return self.__target != other.__target
else:
raise TypeError(
f"Constant with layout {self.__layout!r} can only be compared to another view or "
f"constant with the same layout, not {other!r}")
def __add__(self, other):
raise TypeError("Cannot perform arithmetic operations on a lib.data.Const")
__radd__ = __add__
__sub__ = __add__
__rsub__ = __add__
__mul__ = __add__
__rmul__ = __add__
__floordiv__ = __add__
__rfloordiv__ = __add__
__mod__ = __add__
__rmod__ = __add__
__lshift__ = __add__
__rlshift__ = __add__
__rshift__ = __add__
__rrshift__ = __add__
__lt__ = __add__
__le__ = __add__
__gt__ = __add__
__ge__ = __add__
def __and__(self, other):
raise TypeError("Cannot perform bitwise operations on a lib.data.Const")
__rand__ = __and__
__or__ = __and__
__ror__ = __and__
__xor__ = __and__
__rxor__ = __and__
def __repr__(self):
return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})"
class _AggregateMeta(ShapeCastable, type):
def __new__(metacls, name, bases, namespace):
if "__annotations__" not in namespace:
@ -847,6 +1089,9 @@ class _AggregateMeta(ShapeCastable, type):
fields.update(init or {})
return cls.as_shape().const(fields)
def from_bits(cls, bits):
return cls.as_shape().from_bits(bits)
def _value_repr(cls, value):
return cls.__layout._value_repr(value)
@ -881,7 +1126,7 @@ class Struct(View, metaclass=_AggregateMeta):
def is_subnormal(self):
return self.exponent == 0
The ``IEEE754Single`` class itself can be used where a :ref:`shape <lang-shapes>` is expected:
The :py:`IEEE754Single` class itself can be used where a :ref:`shape <lang-shapes>` is expected:
.. doctest::

View file

@ -173,6 +173,9 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta):
member = cls(init)
return cls(Const(member.value, cls.as_shape()))
def from_bits(cls, bits):
return cls(bits)
def _value_repr(cls, value):
yield Repr(FormatEnum(cls), value)

View file

@ -49,6 +49,7 @@ Implemented RFCs
.. _RFC 45: https://amaranth-lang.org/rfcs/0045-lib-memory.html
.. _RFC 46: https://amaranth-lang.org/rfcs/0046-shape-range-1.html
.. _RFC 50: https://amaranth-lang.org/rfcs/0050-print.html
.. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html
.. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html
.. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html
@ -59,6 +60,7 @@ Implemented RFCs
* `RFC 45`_: Move ``hdl.Memory`` to ``lib.Memory``
* `RFC 46`_: Change ``Shape.cast(range(1))`` to ``unsigned(0)``
* `RFC 50`_: ``Print`` statement and string formatting
* `RFC 51`_: Add ``ShapeCastable.from_bits`` and ``amaranth.lib.data.Const``
* `RFC 53`_: Low-level I/O primitives
@ -70,6 +72,7 @@ Language changes
* Added: :class:`Slice` objects have been made const-castable.
* Added: :func:`amaranth.utils.ceil_log2`, :func:`amaranth.utils.exact_log2`. (`RFC 17`_)
* Added: :class:`Format` objects, :class:`Print` statements, messages in :class:`Assert`, :class:`Assume` and :class:`Cover`. (`RFC 50`_)
* Added: :meth:`ShapeCastable.from_bits` method. (`RFC 51`_)
* Added: IO values, :class:`IOPort` objects, :class:`IOBufferInstance` objects. (`RFC 53`_)
* Changed: ``m.Case()`` with no patterns is never active instead of always active. (`RFC 39`_)
* Changed: ``Value.matches()`` with no patterns is ``Const(0)`` instead of ``Const(1)``. (`RFC 39`_)
@ -94,6 +97,8 @@ Standard library changes
.. currentmodule:: amaranth.lib
* Added: :mod:`amaranth.lib.memory`. (`RFC 45`_)
* Added: :class:`amaranth.lib.data.Const` class. (`RFC 51`_)
* Changed: :meth:`amaranth.lib.data.Layout.const` returns a :class:`amaranth.lib.data.Const`, not a view (`RFC 51`_)
* Added: :class:`amaranth.lib.io.SingleEndedPort`, :class:`amaranth.lib.io.DifferentialPort`. (`RFC 55`_)
* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with ``fwft=False``. (`RFC 20`_)

View file

@ -221,7 +221,7 @@ Modeling structured data
========================
.. autoclass:: Field
.. autoclass:: Layout
.. autoclass:: Layout()
Common data layouts
@ -237,6 +237,7 @@ Data views
==========
.. autoclass:: View
.. autoclass:: Const
Data classes

View file

@ -178,6 +178,9 @@ class MockShapeCastable(ShapeCastable):
def const(self, init):
return Const(init, self.dest)
def from_bits(self, bits):
return bits
class ShapeCastableTestCase(FHDLTestCase):
def test_no_override(self):
@ -208,6 +211,25 @@ class ShapeCastableTestCase(FHDLTestCase):
r"^Can't instantiate abstract class ShapeCastable$"):
ShapeCastable()
def test_no_from_bits(self):
with self.assertWarnsRegex(DeprecationWarning,
r"^Class 'MockShapeCastableNoFromBits' deriving from 'ShapeCastable' does "
r"not override the 'from_bits' method, which will be required in Amaranth 0.6$"):
class MockShapeCastableNoFromBits(ShapeCastable):
def __init__(self, dest):
self.dest = dest
def as_shape(self):
return self.dest
def __call__(self, value):
return value
def const(self, init):
return Const(init, self.dest)
self.assertEqual(MockShapeCastableNoFromBits(unsigned(2)).from_bits(123), 123)
class ShapeLikeTestCase(FHDLTestCase):
def test_construct(self):
@ -514,6 +536,9 @@ class ConstTestCase(FHDLTestCase):
def const(self, init):
return MockConstValue(init)
def from_bits(self, bits):
return bits
s = Const(10, MockConstShape())
self.assertIsInstance(s, MockConstValue)
self.assertEqual(s.value, 10)
@ -1186,6 +1211,9 @@ class SignalTestCase(FHDLTestCase):
def const(self, init):
return int(init, 16)
def from_bits(self, bits):
return bits
s1 = Signal(CastableFromHex(), init="aa")
self.assertEqual(s1.init, 0xaa)

View file

@ -22,6 +22,9 @@ class MockShapeCastable(ShapeCastable):
def const(self, init):
return Const(init, self.shape)
def from_bits(self, bits):
return bits
class FieldTestCase(TestCase):
def test_construct(self):
@ -417,6 +420,9 @@ class LayoutTestCase(FHDLTestCase):
def const(self, init):
return int(init, 16)
def from_bits(self, bits):
return bits
sl = data.StructLayout({"f": CastableFromHex()})
self.assertRepr(sl.const({"f": "aa"}).as_value(), "(const 8'd170)")
@ -467,13 +473,13 @@ class ViewTestCase(FHDLTestCase):
def test_layout_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^View layout must be a layout, not <.+?>$"):
r"^Layout of a view must be a Layout, not <.+?>$"):
data.View(object(), Signal(1))
def test_layout_conflict_with_attr(self):
with self.assertWarnsRegex(SyntaxWarning,
r"^View layout includes a field 'as_value' that will be shadowed by the view "
r"attribute 'amaranth\.lib\.data\.View\.as_value'$"):
r"^Layout of a view includes a field 'as_value' that will be shadowed by "
r"the attribute 'amaranth\.lib\.data\.View\.as_value'$"):
data.View(data.StructLayout({"as_value": unsigned(1)}), Signal(1))
def test_layout_conflict_with_attr_derived(self):
@ -481,20 +487,20 @@ class ViewTestCase(FHDLTestCase):
def foo(self):
pass
with self.assertWarnsRegex(SyntaxWarning,
r"^View layout includes a field 'foo' that will be shadowed by the view "
r"attribute 'tests\.test_lib_data\.ViewTestCase\."
r"^Layout of a view includes a field 'foo' that will be shadowed by "
r"the attribute 'tests\.test_lib_data\.ViewTestCase\."
r"test_layout_conflict_with_attr_derived\.<locals>.DerivedView\.foo'$"):
DerivedView(data.StructLayout({"foo": unsigned(1)}), Signal(1))
def test_target_wrong_type(self):
with self.assertRaisesRegex(TypeError,
r"^View target must be a value-castable object, not <.+?>$"):
r"^Target of a view must be a value-castable object, not <.+?>$"):
data.View(data.StructLayout({}), object())
def test_target_wrong_size(self):
with self.assertRaisesRegex(ValueError,
r"^View target is 2 bit\(s\) wide, which is not compatible with the 1 bit\(s\) "
r"wide view layout$"):
r"^Target of a view is 2 bit\(s\) wide, which is not compatible with its 1 bit\(s\) "
r"wide layout$"):
data.View(data.StructLayout({"a": unsigned(1)}), Signal(2))
def test_getitem(self):
@ -540,6 +546,9 @@ class ViewTestCase(FHDLTestCase):
def const(self, init):
return Const(init, 2)
def from_bits(self, bits):
return bits
v = Signal(data.StructLayout({
"f": Reverser()
}))
@ -557,6 +566,9 @@ class ViewTestCase(FHDLTestCase):
def const(self, init):
return Const(init, 2)
def from_bits(self, bits):
return bits
v = Signal(data.StructLayout({
"f": WrongCastable()
}))
@ -606,14 +618,13 @@ class ViewTestCase(FHDLTestCase):
def test_attr_wrong_missing(self):
with self.assertRaisesRegex(AttributeError,
r"^View of \(sig \$signal\) does not have a field 'a'; "
r"did you mean one of: 'b', 'c'\?$"):
r"^View with layout .* does not have a field 'a'; did you mean one of: 'b', 'c'\?$"):
Signal(data.StructLayout({"b": unsigned(1), "c": signed(1)})).a
def test_attr_wrong_reserved(self):
with self.assertRaisesRegex(AttributeError,
r"^View of \(sig \$signal\) field '_c' has a reserved name "
r"and may only be accessed by indexing$"):
r"^Field '_c' of view with layout .* has a reserved name and may only be accessed "
r"by indexing$"):
Signal(data.StructLayout({"_c": signed(1)}))._c
def test_signal_like(self):
@ -623,13 +634,13 @@ class ViewTestCase(FHDLTestCase):
def test_bug_837_array_layout_getitem_str(self):
with self.assertRaisesRegex(TypeError,
r"^Views with array layout may only be indexed with an integer or a value, "
r"^View with array layout may only be indexed with an integer or a value, "
r"not 'init'$"):
Signal(data.ArrayLayout(unsigned(1), 1), init=[0])["init"]
def test_bug_837_array_layout_getattr(self):
with self.assertRaisesRegex(AttributeError,
r"^View of \(sig \$signal\) with an array layout does not have fields$"):
r"^View with an array layout does not have fields$"):
Signal(data.ArrayLayout(unsigned(1), 1), init=[0]).init
def test_eq(self):
@ -639,16 +650,20 @@ class ViewTestCase(FHDLTestCase):
self.assertRepr(s1 == s2, "(== (sig s1) (sig s2))")
self.assertRepr(s1 != s2, "(!= (sig s1) (sig s2))")
with self.assertRaisesRegex(TypeError,
r"^View of .* can only be compared to another view of the same layout, not .*$"):
r"^View with layout .* can only be compared to another view or constant "
r"with the same layout, not .*$"):
s1 == s3
with self.assertRaisesRegex(TypeError,
r"^View of .* can only be compared to another view of the same layout, not .*$"):
r"^View with layout .* can only be compared to another view or constant "
r"with the same layout, not .*$"):
s1 != s3
with self.assertRaisesRegex(TypeError,
r"^View of .* can only be compared to another view of the same layout, not .*$"):
r"^View with layout .* can only be compared to another view or constant "
r"with the same layout, not .*$"):
s1 == Const(0, 2)
with self.assertRaisesRegex(TypeError,
r"^View of .* can only be compared to another view of the same layout, not .*$"):
r"^View with layout .* can only be compared to another view or constant "
r"with the same layout, not .*$"):
s1 != Const(0, 2)
def test_operator(self):
@ -690,6 +705,251 @@ class ViewTestCase(FHDLTestCase):
self.assertRepr(s1, "View(StructLayout({'a': unsigned(2)}), (sig s1))")
class ConstTestCase(FHDLTestCase):
def test_construct(self):
c = data.Const(data.StructLayout({"a": unsigned(1), "b": unsigned(2)}), 5)
self.assertRepr(Value.cast(c), "(const 3'd5)")
self.assertEqual(c.shape(), data.StructLayout({"a": unsigned(1), "b": unsigned(2)}))
self.assertEqual(c.as_bits(), 5)
self.assertEqual(c["a"], 1)
self.assertEqual(c["b"], 2)
def test_construct_const(self):
c = Const({"a": 1, "b": 2}, data.StructLayout({"a": unsigned(1), "b": unsigned(2)}))
self.assertRepr(Const.cast(c), "(const 3'd5)")
self.assertEqual(c.a, 1)
self.assertEqual(c.b, 2)
def test_layout_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^Layout of a constant must be a Layout, not <.+?>$"):
data.Const(object(), 1)
def test_layout_conflict_with_attr(self):
with self.assertWarnsRegex(SyntaxWarning,
r"^Layout of a constant includes a field 'as_value' that will be shadowed by "
r"the attribute 'amaranth\.lib\.data\.Const\.as_value'$"):
data.Const(data.StructLayout({"as_value": unsigned(1)}), 1)
def test_layout_conflict_with_attr_derived(self):
class DerivedConst(data.Const):
def foo(self):
pass
with self.assertWarnsRegex(SyntaxWarning,
r"^Layout of a constant includes a field 'foo' that will be shadowed by "
r"the attribute 'tests\.test_lib_data\.ConstTestCase\."
r"test_layout_conflict_with_attr_derived\.<locals>.DerivedConst\.foo'$"):
DerivedConst(data.StructLayout({"foo": unsigned(1)}), 1)
def test_target_wrong_type(self):
with self.assertRaisesRegex(TypeError,
r"^Target of a constant must be an int, not <.+?>$"):
data.Const(data.StructLayout({}), object())
def test_target_wrong_value(self):
with self.assertRaisesRegex(ValueError,
r"^Target of a constant does not fit in 1 bit\(s\)$"):
data.Const(data.StructLayout({"a": unsigned(1)}), 2)
def test_getitem(self):
l = data.StructLayout({
"u": unsigned(1),
"v": unsigned(1)
})
v = data.Const(data.StructLayout({
"a": unsigned(2),
"s": data.StructLayout({
"b": unsigned(1),
"c": unsigned(3)
}),
"p": 1,
"q": signed(1),
"r": data.ArrayLayout(unsigned(2), 2),
"t": data.ArrayLayout(data.StructLayout({
"u": unsigned(1),
"v": unsigned(1)
}), 2),
}), 0xabcd)
cv = Value.cast(v)
i = Signal(1)
self.assertEqual(cv.shape(), unsigned(16))
self.assertEqual(v["a"], 1)
self.assertEqual(v["s"]["b"], 1)
self.assertEqual(v["s"]["c"], 1)
self.assertEqual(v["p"], 1)
self.assertEqual(v["q"], -1)
self.assertEqual(v["r"][0], 3)
self.assertEqual(v["r"][1], 2)
self.assertRepr(v["r"][i], "(part (const 4'd11) (sig i) 2 2)")
self.assertEqual(v["t"][0], data.Const(l, 2))
self.assertEqual(v["t"][1], data.Const(l, 2))
self.assertEqual(v["t"][0]["u"], 0)
self.assertEqual(v["t"][1]["v"], 1)
def test_getitem_custom_call(self):
class Reverser(ShapeCastable):
def as_shape(self):
return unsigned(2)
def __call__(self, value):
raise NotImplementedError
def const(self, init):
raise NotImplementedError
def from_bits(self, bits):
return float(bits) / 2
v = data.Const(data.StructLayout({
"f": Reverser()
}), 3)
self.assertEqual(v.f, 1.5)
def test_index_wrong_missing(self):
with self.assertRaisesRegex(KeyError,
r"^'a'$"):
data.Const(data.StructLayout({}), 0)["a"]
def test_index_wrong_struct_dynamic(self):
with self.assertRaisesRegex(TypeError,
r"^Only constants with array layout, not StructLayout\(\{\}\), may be indexed "
r"with a value$"):
data.Const(data.StructLayout({}), 0)[Signal(1)]
def test_getattr(self):
v = data.Const(data.UnionLayout({
"a": unsigned(2),
"s": data.StructLayout({
"b": unsigned(1),
"c": unsigned(3)
}),
"p": 1,
"q": signed(1),
}), 13)
cv = Const.cast(v)
i = Signal(1)
self.assertEqual(cv.shape(), unsigned(4))
self.assertEqual(v.a, 1)
self.assertEqual(v.s.b, 1)
self.assertEqual(v.s.c, 6)
self.assertEqual(v.p, 1)
self.assertEqual(v.q, -1)
def test_getattr_reserved(self):
v = data.Const(data.UnionLayout({
"_a": unsigned(2)
}), 2)
self.assertEqual(v["_a"], 2)
def test_attr_wrong_missing(self):
with self.assertRaisesRegex(AttributeError,
r"^Constant with layout .* does not have a field 'a'; did you mean one of: "
r"'b', 'c'\?$"):
data.Const(data.StructLayout({"b": unsigned(1), "c": signed(1)}), 0).a
def test_attr_wrong_reserved(self):
with self.assertRaisesRegex(AttributeError,
r"^Field '_c' of constant with layout .* has a reserved name and may only be "
r"accessed by indexing$"):
data.Const(data.StructLayout({"_c": signed(1)}), 0)._c
def test_bug_837_array_layout_getitem_str(self):
with self.assertRaisesRegex(TypeError,
r"^Constant with array layout may only be indexed with an integer or a value, "
r"not 'init'$"):
data.Const(data.ArrayLayout(unsigned(1), 1), 0)["init"]
def test_bug_837_array_layout_getattr(self):
with self.assertRaisesRegex(AttributeError,
r"^Constant with an array layout does not have fields$"):
data.Const(data.ArrayLayout(unsigned(1), 1), 0).init
def test_eq(self):
c1 = data.Const(data.StructLayout({"a": unsigned(2)}), 1)
c2 = data.Const(data.StructLayout({"a": unsigned(2)}), 1)
c3 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
c4 = data.Const(data.StructLayout({"a": unsigned(1), "b": unsigned(1)}), 2)
s1 = Signal(data.StructLayout({"a": unsigned(2)}))
self.assertTrue(c1 == c2)
self.assertFalse(c1 != c2)
self.assertFalse(c1 == c3)
self.assertTrue(c1 != c3)
self.assertRepr(c1 == s1, "(== (const 2'd1) (sig s1))")
self.assertRepr(c1 != s1, "(!= (const 2'd1) (sig s1))")
self.assertRepr(s1 == c1, "(== (sig s1) (const 2'd1))")
self.assertRepr(s1 != c1, "(!= (sig s1) (const 2'd1))")
with self.assertRaisesRegex(TypeError,
r"^Constant with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
c1 == c4
with self.assertRaisesRegex(TypeError,
r"^Constant with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
c1 != c4
with self.assertRaisesRegex(TypeError,
r"^View with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
s1 == c4
with self.assertRaisesRegex(TypeError,
r"^View with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
s1 != c4
with self.assertRaisesRegex(TypeError,
r"^Constant with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
c4 == s1
with self.assertRaisesRegex(TypeError,
r"^Constant with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
c4 != s1
with self.assertRaisesRegex(TypeError,
r"^Constant with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
c1 == Const(0, 2)
with self.assertRaisesRegex(TypeError,
r"^Constant with layout .* can only be compared to another view or constant with "
r"the same layout, not .*$"):
c1 != Const(0, 2)
def test_operator(self):
s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
s2 = Signal(unsigned(2))
for op in [
operator.__add__,
operator.__sub__,
operator.__mul__,
operator.__floordiv__,
operator.__mod__,
operator.__lshift__,
operator.__rshift__,
operator.__lt__,
operator.__le__,
operator.__gt__,
operator.__ge__,
]:
with self.assertRaisesRegex(TypeError,
r"^Cannot perform arithmetic operations on a lib.data.Const$"):
op(s1, s2)
with self.assertRaisesRegex(TypeError,
r"^Cannot perform arithmetic operations on a lib.data.Const$"):
op(s2, s1)
for op in [
operator.__and__,
operator.__or__,
operator.__xor__,
]:
with self.assertRaisesRegex(TypeError,
r"^Cannot perform bitwise operations on a lib.data.Const$"):
op(s1, s2)
with self.assertRaisesRegex(TypeError,
r"^Cannot perform bitwise operations on a lib.data.Const$"):
op(s2, s1)
def test_repr(self):
s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
self.assertRepr(s1, "Const(StructLayout({'a': unsigned(2)}), 2)")
class StructTestCase(FHDLTestCase):
def test_construct(self):
class S(data.Struct):
@ -815,6 +1075,13 @@ class StructTestCase(FHDLTestCase):
s2 = Signal.like(s1)
self.assertEqual(s2.shape(), S)
def test_from_bits(self):
class S(data.Struct):
a: 1
c = S.from_bits(1)
self.assertIsInstance(c, data.Const)
self.assertEqual(c.a, 1)
class UnionTestCase(FHDLTestCase):
def test_construct(self):

View file

@ -110,6 +110,15 @@ class EnumTestCase(FHDLTestCase):
self.assertRepr(EnumA.const(10), "EnumView(EnumA, (const 8'd10))")
self.assertRepr(EnumA.const(EnumA.A), "EnumView(EnumA, (const 8'd10))")
def test_from_bits(self):
class EnumA(Enum, shape=2):
A = 0
B = 1
C = 2
self.assertIs(EnumA.from_bits(2), EnumA.C)
with self.assertRaises(ValueError):
EnumA.from_bits(3)
def test_shape_implicit_wrong_in_concat(self):
class EnumA(Enum):
A = 0