Implement RFC 51: Add ShapeCastable.from_bits
and amaranth.lib.data.Const
.
Co-authored-by: Catherine <whitequark@whitequark.org>
This commit is contained in:
parent
598cf8db28
commit
d6bf47d549
|
@ -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)
|
||||
|
||||
|
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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`_)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue