From d6bf47d5496da964afbd306f9ac3ca900607f04d Mon Sep 17 00:00:00 2001 From: Wanda Date: Thu, 14 Mar 2024 20:07:53 +0100 Subject: [PATCH] Implement RFC 51: Add `ShapeCastable.from_bits` and `amaranth.lib.data.Const`. Co-authored-by: Catherine --- amaranth/hdl/_ast.py | 38 ++++ amaranth/lib/data.py | 425 ++++++++++++++++++++++++++++++++--------- amaranth/lib/enum.py | 3 + docs/changes.rst | 5 + docs/stdlib/data.rst | 3 +- tests/test_hdl_ast.py | 28 +++ tests/test_lib_data.py | 303 +++++++++++++++++++++++++++-- tests/test_lib_enum.py | 9 + 8 files changed, 705 insertions(+), 109 deletions(-) diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py index dc4d3d4..e8a6553 100644 --- a/amaranth/hdl/_ast.py +++ b/amaranth/hdl/_ast.py @@ -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) diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py index afd775e..a3e6199 100644 --- a/amaranth/lib/data.py +++ b/amaranth/lib/data.py @@ -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 ` + 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 ` 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 ` 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 ` + 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 ` + 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 ` + 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 ` 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 ` + 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 ` 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 ` 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 ` is expected: + The :py:`IEEE754Single` class itself can be used where a :ref:`shape ` is expected: .. doctest:: diff --git a/amaranth/lib/enum.py b/amaranth/lib/enum.py index 135bb58..adf94d6 100644 --- a/amaranth/lib/enum.py +++ b/amaranth/lib/enum.py @@ -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) diff --git a/docs/changes.rst b/docs/changes.rst index cca07b0..1ba3761 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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`_) diff --git a/docs/stdlib/data.rst b/docs/stdlib/data.rst index 0161790..198e8b1 100644 --- a/docs/stdlib/data.rst +++ b/docs/stdlib/data.rst @@ -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 diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py index 47d89cc..d477855 100644 --- a/tests/test_hdl_ast.py +++ b/tests/test_hdl_ast.py @@ -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) diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py index 071692e..19c46db 100644 --- a/tests/test_lib_data.py +++ b/tests/test_lib_data.py @@ -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\..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\..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): diff --git a/tests/test_lib_enum.py b/tests/test_lib_enum.py index f4fad0d..6d7799b 100644 --- a/tests/test_lib_enum.py +++ b/tests/test_lib_enum.py @@ -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