Implement RFC 35: Add ShapeLike, ValueLike.

This commit is contained in:
Wanda 2023-12-03 04:03:13 +01:00 committed by Catherine
parent 422ba9ea51
commit e9545efb22
7 changed files with 192 additions and 30 deletions

View file

@ -4,7 +4,7 @@ import warnings
import functools import functools
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence
from enum import Enum from enum import Enum, EnumMeta
from itertools import chain from itertools import chain
from ._repr import * from ._repr import *
@ -15,11 +15,11 @@ from .._unused import *
__all__ = [ __all__ = [
"Shape", "signed", "unsigned", "ShapeCastable", "Shape", "signed", "unsigned", "ShapeCastable", "ShapeLike",
"Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl", "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl",
"Array", "ArrayProxy", "Array", "ArrayProxy",
"Signal", "ClockSignal", "ResetSignal", "Signal", "ClockSignal", "ResetSignal",
"ValueCastable", "ValueCastable", "ValueLike",
"Sample", "Past", "Stable", "Rose", "Fell", "Initial", "Sample", "Past", "Stable", "Rose", "Fell", "Initial",
"Statement", "Switch", "Statement", "Switch",
"Property", "Assign", "Assert", "Assume", "Cover", "Property", "Assign", "Assert", "Assume", "Cover",
@ -150,6 +150,52 @@ class Shape:
self.width == other.width and self.signed == other.signed) self.width == other.width and self.signed == other.signed)
class _ShapeLikeMeta(type):
def __subclasscheck__(cls, subclass):
return issubclass(subclass, (Shape, ShapeCastable, int, range, EnumMeta)) or subclass is ShapeLike
def __instancecheck__(cls, instance):
if isinstance(instance, (Shape, ShapeCastable, range)):
return True
if isinstance(instance, int):
return instance >= 0
if isinstance(instance, EnumMeta):
for member in instance:
if not isinstance(member.value, ValueLike):
return False
return True
return False
@final
class ShapeLike(metaclass=_ShapeLikeMeta):
"""An abstract class representing all objects that can be cast to a :class:`Shape`.
``issubclass(cls, ShapeLike)`` returns ``True`` for:
- :class:`Shape`
- :class:`ShapeCastable` and its subclasses
- ``int`` and its subclasses
- ``range`` and its subclasses
- :class:`enum.EnumMeta` and its subclasses
- :class:`ShapeLike` itself
``isinstance(obj, ShapeLike)`` returns ``True`` for:
- :class:`Shape` instances
- :class:`ShapeCastable` instances
- non-negative ``int`` values
- ``range`` instances
- :class:`enum.Enum` subclasses where all values are :ref:`value-like <lang-valuelike>`
This class is only usable for the above checks no instances and no (non-virtual)
subclasses can be created.
"""
def __new__(cls, *args, **kwargs):
raise TypeError("ShapeLike is an abstract class and cannot be constructed")
def unsigned(width): def unsigned(width):
"""Shorthand for ``Shape(width, signed=False)``.""" """Shorthand for ``Shape(width, signed=False)``."""
return Shape(width, signed=False) return Shape(width, signed=False)
@ -1479,6 +1525,40 @@ class ValueCastable:
return wrapper_memoized return wrapper_memoized
class _ValueLikeMeta(type):
"""An abstract class representing all objects that can be cast to a :class:`Value`.
``issubclass(cls, ValueLike)`` returns ``True`` for:
- :class:`Value`
- :class:`ValueCastable` and its subclasses
- ``int`` and its subclasses
- :class:`enum.Enum` subclasses where all values are :ref:`value-like <lang-valuelike>`
- :class:`ValueLike` itself
``isinstance(obj, ValueLike)`` returns the same value as ``issubclass(type(obj), ValueLike)``.
This class is only usable for the above checks no instances and no (non-virtual)
subclasses can be created.
"""
def __subclasscheck__(cls, subclass):
if issubclass(subclass, (Value, ValueCastable, int)) or subclass is ValueLike:
return True
if issubclass(subclass, Enum):
return isinstance(subclass, ShapeLike)
return False
def __instancecheck__(cls, instance):
return issubclass(type(instance), cls)
@final
class ValueLike(metaclass=_ValueLikeMeta):
def __new__(cls, *args, **kwargs):
raise TypeError("ValueLike is an abstract class and cannot be constructed")
# TODO(amaranth-0.5): remove # TODO(amaranth-0.5): remove
@final @final
class Sample(Value): class Sample(Value):

View file

@ -26,7 +26,7 @@ class Field:
Attributes Attributes
---------- ----------
shape : :ref:`shape-castable <lang-shapecasting>` shape : :ref:`shape-like <lang-shapelike>`
Shape of the field. When initialized or assigned, the object is stored as-is. Shape of the field. When initialized or assigned, the object is stored as-is.
offset : :class:`int`, >=0 offset : :class:`int`, >=0
Index of the least significant bit of the field. Index of the least significant bit of the field.
@ -56,7 +56,7 @@ class Field:
"""Width of the field. """Width of the field.
This property should be used over ``self.shape.width`` because ``self.shape`` can be This property should be used over ``self.shape.width`` because ``self.shape`` can be
an arbitrary :ref:`shape-castable <lang-shapecasting>` object, which may not have an arbitrary :ref:`shape-like <lang-shapelike>` object, which may not have
a ``width`` property. a ``width`` property.
Returns Returns
@ -82,7 +82,7 @@ class Field:
class Layout(ShapeCastable, metaclass=ABCMeta): class Layout(ShapeCastable, metaclass=ABCMeta):
"""Description of a data layout. """Description of a data layout.
The :ref:`shape-castable <lang-shapecasting>` :class:`Layout` interface associates keys The :ref:`shape-like <lang-shapelike>` :class:`Layout` interface associates keys
(string names or integer indexes) with fields, giving identifiers to spans of bits in (string names or integer indexes) with fields, giving identifiers to spans of bits in
an Amaranth value. an Amaranth value.
@ -96,7 +96,7 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
@staticmethod @staticmethod
def cast(obj): def cast(obj):
"""Cast a :ref:`shape-castable <lang-shapecasting>` object to a layout. """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 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. recursively call ``.as_shape()``, but only until a layout is returned.
@ -279,7 +279,7 @@ class StructLayout(Layout):
Attributes Attributes
---------- ----------
members : mapping of :class:`str` to :ref:`shape-castable <lang-shapecasting>` members : mapping of :class:`str` to :ref:`shape-like <lang-shapelike>`
Dictionary of structure members. Dictionary of structure members.
""" """
@ -350,7 +350,7 @@ class UnionLayout(Layout):
Attributes Attributes
---------- ----------
members : mapping of :class:`str` to :ref:`shape-castable <lang-shapecasting>` members : mapping of :class:`str` to :ref:`shape-like <lang-shapelike>`
Dictionary of union members. Dictionary of union members.
""" """
def __init__(self, members): def __init__(self, members):
@ -425,7 +425,7 @@ class ArrayLayout(Layout):
Attributes Attributes
---------- ----------
elem_shape : :ref:`shape-castable <lang-shapecasting>` elem_shape : :ref:`shape-like <lang-shapelike>`
Shape of an individual element. Shape of an individual element.
length : :class:`int` length : :class:`int`
Amount of elements. Amount of elements.
@ -567,7 +567,7 @@ class FlexibleLayout(Layout):
class View(ValueCastable): class View(ValueCastable):
"""A value viewed through the lens of a layout. """A value viewed through the lens of a layout.
The :ref:`value-castable <lang-valuecasting>` class :class:`View` provides access to the fields The :ref:`value-like <lang-valuelike>` class :class:`View` provides access to the fields
of an underlying Amaranth value via the names or indexes defined in the provided layout. of an underlying Amaranth value via the names or indexes defined in the provided layout.
Creating a view Creating a view
@ -583,7 +583,7 @@ class View(ValueCastable):
a value-castable object. If the shape of the field is a :class:`Layout`, it will be 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 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 will be an instance of that data class; if it is another
:ref:`shape-castable <lang-shapecasting>` object implementing ``__call__``, it will be :ref:`shape-like <lang-shapelike>` object implementing ``__call__``, it will be
the result of calling that method. the result of calling that method.
Slicing a view whose layout is an :class:`ArrayLayout` can be done with an index that is Slicing a view whose layout is an :class:`ArrayLayout` can be done with an index that is
@ -859,7 +859,7 @@ class Struct(View, metaclass=_AggregateMeta):
to describe the structure layout and reset values for the fields using Python to describe the structure layout and reset values for the fields using Python
:term:`variable annotations <python:variable annotation>`. :term:`variable annotations <python:variable annotation>`.
Any annotations containing :ref:`shape-castable <lang-shapecasting>` objects are used, Any annotations containing :ref:`shape-like <lang-shapelike>` objects are used,
in the order in which they appear in the source code, to construct a :class:`StructLayout`. in the order in which they appear in the source code, to construct a :class:`StructLayout`.
The values assigned to such annotations are used to populate the reset value of the signal The values assigned to such annotations are used to populate the reset value of the signal
created by the view. Any other annotations are kept as-is. created by the view. Any other annotations are kept as-is.

View file

@ -19,13 +19,13 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta):
protocol. protocol.
This metaclass provides the :meth:`as_shape` method, making its instances This metaclass provides the :meth:`as_shape` method, making its instances
:ref:`shape-castable <lang-shapecasting>`, and accepts a ``shape=`` keyword argument :ref:`shape-like <lang-shapelike>`, and accepts a ``shape=`` keyword argument
to specify a shape explicitly. Other than this, it acts the same as the standard to specify a shape explicitly. Other than this, it acts the same as the standard
:class:`enum.EnumMeta` class; if the ``shape=`` argument is not specified and :class:`enum.EnumMeta` class; if the ``shape=`` argument is not specified and
:meth:`as_shape` is never called, it places no restrictions on the enumeration class :meth:`as_shape` is never called, it places no restrictions on the enumeration class
or the values of its members. or the values of its members.
When a :ref:`value-castable <lang-valuecasting>` is cast to an enum type that is an instance When a :ref:`value-like <lang-valuelike>` is cast to an enum type that is an instance
of this metaclass, it can be automatically wrapped in a view class. A custom view class of this metaclass, it can be automatically wrapped in a view class. A custom view class
can be specified by passing the ``view_class=`` keyword argument when creating the enum class. can be specified by passing the ``view_class=`` keyword argument when creating the enum class.
""" """
@ -139,7 +139,7 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta):
When given an integer constant, it returns the corresponding enum value, like a standard When given an integer constant, it returns the corresponding enum value, like a standard
Python enumeration. Python enumeration.
When given a :ref:`value-castable <lang-valuecasting>`, it is cast to a value, then wrapped When given a :ref:`value-like <lang-valuelike>`, it is cast to a value, then wrapped
in the ``view_class`` specified for this enum type (:class:`EnumView` for :class:`Enum`, in the ``view_class`` specified for this enum type (:class:`EnumView` for :class:`Enum`,
:class:`FlagView` for :class:`Flag`, or a custom user-defined class). If the type has no :class:`FlagView` for :class:`Flag`, or a custom user-defined class). If the type has no
``view_class`` (like :class:`IntEnum` or :class:`IntFlag`), a plain ``view_class`` (like :class:`IntEnum` or :class:`IntFlag`), a plain
@ -214,7 +214,7 @@ class EnumView(ValueCastable):
def __init__(self, enum, target): def __init__(self, enum, target):
"""Constructs a view with the given enum type and target """Constructs a view with the given enum type and target
(a :ref:`value-castable <lang-valuecasting>`). (a :ref:`value-like <lang-valuelike>`).
""" """
if not isinstance(enum, EnumMeta) or not hasattr(enum, "_amaranth_shape_"): if not isinstance(enum, EnumMeta) or not hasattr(enum, "_amaranth_shape_"):
raise TypeError(f"EnumView type must be an enum with shape, not {enum!r}") raise TypeError(f"EnumView type must be an enum with shape, not {enum!r}")
@ -312,7 +312,7 @@ class FlagView(EnumView):
values of the same enum type.""" values of the same enum type."""
def __invert__(self): def __invert__(self):
"""Inverts all flags in this value and returns another :ref:`FlagView`. """Inverts all flags in this value and returns another :class:`FlagView`.
Note that this is not equivalent to applying bitwise negation to the underlying value: Note that this is not equivalent to applying bitwise negation to the underlying value:
just like the Python :class:`enum.Flag` class, only bits corresponding to flags actually just like the Python :class:`enum.Flag` class, only bits corresponding to flags actually

View file

@ -121,14 +121,14 @@ The shape of the constant can be specified explicitly, in which case the number'
0 0
.. _lang-shapecasting: .. _lang-shapelike:
Shape casting Shape casting
============= =============
Shapes can be *cast* from other objects, which are called *shape-castable*. Casting is a convenient way to specify a shape indirectly, for example, by a range of numbers representable by values with that shape. Shapes can be *cast* from other objects, which are called *shape-like*. Casting is a convenient way to specify a shape indirectly, for example, by a range of numbers representable by values with that shape.
Casting to a shape can be done explicitly with ``Shape.cast``, but is usually implicit, since shape-castable objects are accepted anywhere shapes are. Casting to a shape can be done explicitly with ``Shape.cast``, but is usually implicit, since shape-like objects are accepted anywhere shapes are.
.. _lang-shapeint: .. _lang-shapeint:
@ -244,16 +244,16 @@ The :mod:`amaranth.lib.enum` module extends the standard enumerations such that
The enumeration does not have to subclass :class:`enum.IntEnum` or have :class:`int` as one of its base classes; it only needs to have integers as values of every member. Using enumerations based on :class:`enum.Enum` rather than :class:`enum.IntEnum` prevents unwanted implicit conversion of enum members to integers. The enumeration does not have to subclass :class:`enum.IntEnum` or have :class:`int` as one of its base classes; it only needs to have integers as values of every member. Using enumerations based on :class:`enum.Enum` rather than :class:`enum.IntEnum` prevents unwanted implicit conversion of enum members to integers.
.. _lang-valuecasting: .. _lang-valuelike:
Value casting Value casting
============= =============
Like shapes, values may be *cast* from other objects, which are called *value-castable*. Casting to values allows objects that are not provided by Amaranth, such as integers or enumeration members, to be used in Amaranth expressions directly. Like shapes, values may be *cast* from other objects, which are called *value-like*. Casting to values allows objects that are not provided by Amaranth, such as integers or enumeration members, to be used in Amaranth expressions directly.
.. TODO: link to ValueCastable .. TODO: link to ValueCastable
Casting to a value can be done explicitly with ``Value.cast``, but is usually implicit, since value-castable objects are accepted anywhere values are. Casting to a value can be done explicitly with ``Value.cast``, but is usually implicit, since value-like objects are accepted anywhere values are.
Values from integers Values from integers
@ -343,7 +343,7 @@ A *signal* is a value representing a (potentially) varying number. Signals can b
Signal shapes Signal shapes
------------- -------------
A signal can be created with an explicitly specified shape (any :ref:`shape-castable <lang-shapecasting>` object); if omitted, the shape defaults to ``unsigned(1)``. Although rarely useful, 0-bit signals are permitted. A signal can be created with an explicitly specified shape (any :ref:`shape-like <lang-shapelike>` object); if omitted, the shape defaults to ``unsigned(1)``. Although rarely useful, 0-bit signals are permitted.
.. doctest:: .. doctest::
@ -444,7 +444,7 @@ Amaranth provides aggregate data structures in the standard library module :mod:
Operators Operators
========= =========
To describe computations, Amaranth values can be combined with each other or with :ref:`value-castable <lang-valuecasting>` objects using a rich array of arithmetic, bitwise, logical, bit sequence, and other *operators* to form *expressions*, which are themselves values. To describe computations, Amaranth values can be combined with each other or with :ref:`value-like <lang-valuelike>` objects using a rich array of arithmetic, bitwise, logical, bit sequence, and other *operators* to form *expressions*, which are themselves values.
.. _lang-abstractexpr: .. _lang-abstractexpr:

View file

@ -61,7 +61,7 @@ While this implementation works, it is repetitive, error-prone, hard to read, an
m.d.comb += o_gray.eq((i_color.red + i_color.green + i_color.blue) << 1) m.d.comb += o_gray.eq((i_color.red + i_color.green + i_color.blue) << 1)
The :class:`View` is :ref:`value-castable <lang-valuecasting>` and can be used anywhere a plain value can be used. For example, it can be assigned to in the usual way: The :class:`View` is :ref:`value-like <lang-valuelike>` and can be used anywhere a plain value can be used. For example, it can be assigned to in the usual way:
.. testcode:: .. testcode::
@ -135,7 +135,7 @@ In case the data has related operations or transformations, :class:`View` can be
def brightness(self): def brightness(self):
return (self.red + self.green + self.blue)[-8:] return (self.red + self.green + self.blue)[-8:]
Here, the ``RGBLayout`` class itself is :ref:`shape-castable <lang-shapecasting>` and can be used anywhere a shape is accepted. When a :class:`Signal` is constructed with this layout, the returned value is wrapped in an ``RGBView``: Here, the ``RGBLayout`` class itself is :ref:`shape-like <lang-shapelike>` and can be used anywhere a shape is accepted. When a :class:`Signal` is constructed with this layout, the returned value is wrapped in an ``RGBView``:
.. doctest:: .. doctest::

View file

@ -59,7 +59,7 @@ The ``shape=`` argument is optional. If not specified, classes from this module
In this way, this module is a drop-in replacement for the standard :mod:`enum` module, and in an Amaranth project, all ``import enum`` statements may be replaced with ``from amaranth.lib import enum``. In this way, this module is a drop-in replacement for the standard :mod:`enum` module, and in an Amaranth project, all ``import enum`` statements may be replaced with ``from amaranth.lib import enum``.
Signals with :class:`Enum` or :class:`Flag` based shape are automatically wrapped in the :class:`EnumView` or :class:`FlagView` value-castable wrappers, which ensure type safety. Any :ref:`value-castable <lang-valuecasting>` can also be explicitly wrapped in a view class by casting it to the enum type: Signals with :class:`Enum` or :class:`Flag` based shape are automatically wrapped in the :class:`EnumView` or :class:`FlagView` value-like wrappers, which ensure type safety. Any :ref:`value-like <lang-valuelike>` can also be explicitly wrapped in a view class by casting it to the enum type:
.. doctest:: .. doctest::

View file

@ -1,5 +1,5 @@
import warnings import warnings
from enum import Enum from enum import Enum, EnumMeta
from amaranth.hdl.ast import * from amaranth.hdl.ast import *
from amaranth.lib.enum import Enum as AmaranthEnum from amaranth.lib.enum import Enum as AmaranthEnum
@ -189,6 +189,44 @@ class ShapeCastableTestCase(FHDLTestCase):
self.assertEqual(Shape.cast(sc), unsigned(1)) self.assertEqual(Shape.cast(sc), unsigned(1))
class ShapeLikeTestCase(FHDLTestCase):
def test_construct(self):
with self.assertRaises(TypeError):
ShapeLike()
def test_subclass(self):
self.assertTrue(issubclass(Shape, ShapeLike))
self.assertTrue(issubclass(MockShapeCastable, ShapeLike))
self.assertTrue(issubclass(int, ShapeLike))
self.assertTrue(issubclass(range, ShapeLike))
self.assertTrue(issubclass(EnumMeta, ShapeLike))
self.assertFalse(issubclass(Enum, ShapeLike))
self.assertFalse(issubclass(str, ShapeLike))
self.assertTrue(issubclass(ShapeLike, ShapeLike))
def test_isinstance(self):
self.assertTrue(isinstance(unsigned(2), ShapeLike))
self.assertTrue(isinstance(MockShapeCastable(unsigned(2)), ShapeLike))
self.assertTrue(isinstance(2, ShapeLike))
self.assertTrue(isinstance(0, ShapeLike))
self.assertFalse(isinstance(-1, ShapeLike))
self.assertTrue(isinstance(range(10), ShapeLike))
self.assertFalse(isinstance("abc", ShapeLike))
def test_isinstance_enum(self):
class EnumA(Enum):
A = 1
B = 2
class EnumB(Enum):
A = "a"
B = "b"
class EnumC(Enum):
A = Cat(Const(1, 2), Const(0, 2))
self.assertTrue(isinstance(EnumA, ShapeLike))
self.assertFalse(isinstance(EnumB, ShapeLike))
self.assertTrue(isinstance(EnumC, ShapeLike))
class ValueTestCase(FHDLTestCase): class ValueTestCase(FHDLTestCase):
def test_cast(self): def test_cast(self):
self.assertIsInstance(Value.cast(0), Const) self.assertIsInstance(Value.cast(0), Const)
@ -1300,6 +1338,50 @@ class ValueCastableTestCase(FHDLTestCase):
self.assertIsInstance(Value.cast(vc), Signal) self.assertIsInstance(Value.cast(vc), Signal)
class ValueLikeTestCase(FHDLTestCase):
def test_construct(self):
with self.assertRaises(TypeError):
ValueLike()
def test_subclass(self):
self.assertTrue(issubclass(Value, ValueLike))
self.assertTrue(issubclass(MockValueCastable, ValueLike))
self.assertTrue(issubclass(int, ValueLike))
self.assertFalse(issubclass(range, ValueLike))
self.assertFalse(issubclass(EnumMeta, ValueLike))
self.assertTrue(issubclass(Enum, ValueLike))
self.assertFalse(issubclass(str, ValueLike))
self.assertTrue(issubclass(ValueLike, ValueLike))
def test_isinstance(self):
self.assertTrue(isinstance(Const(0, 2), ValueLike))
self.assertTrue(isinstance(MockValueCastable(Const(0, 2)), ValueLike))
self.assertTrue(isinstance(2, ValueLike))
self.assertTrue(isinstance(-2, ValueLike))
self.assertFalse(isinstance(range(10), ValueLike))
def test_enum(self):
class EnumA(Enum):
A = 1
B = 2
class EnumB(Enum):
A = "a"
B = "b"
class EnumC(Enum):
A = Cat(Const(1, 2), Const(0, 2))
class EnumD(Enum):
A = 1
B = "a"
self.assertTrue(issubclass(EnumA, ValueLike))
self.assertFalse(issubclass(EnumB, ValueLike))
self.assertTrue(issubclass(EnumC, ValueLike))
self.assertFalse(issubclass(EnumD, ValueLike))
self.assertTrue(isinstance(EnumA.A, ValueLike))
self.assertFalse(isinstance(EnumB.A, ValueLike))
self.assertTrue(isinstance(EnumC.A, ValueLike))
self.assertFalse(isinstance(EnumD.A, ValueLike))
class SampleTestCase(FHDLTestCase): class SampleTestCase(FHDLTestCase):
@_ignore_deprecated @_ignore_deprecated
def test_const(self): def test_const(self):