diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py index 6d5107eec..c698f0c20 100644 --- a/amaranth/hdl/ast.py +++ b/amaranth/hdl/ast.py @@ -4,7 +4,7 @@ import functools from collections import OrderedDict from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence -from enum import Enum +from enum import Enum, EnumMeta from itertools import chain from ._repr import * @@ -15,11 +15,11 @@ __all__ = [ - "Shape", "signed", "unsigned", "ShapeCastable", + "Shape", "signed", "unsigned", "ShapeCastable", "ShapeLike", "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl", "Array", "ArrayProxy", "Signal", "ClockSignal", "ResetSignal", - "ValueCastable", + "ValueCastable", "ValueLike", "Sample", "Past", "Stable", "Rose", "Fell", "Initial", "Statement", "Switch", "Property", "Assign", "Assert", "Assume", "Cover", @@ -150,6 +150,52 @@ def __eq__(self, other): 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 ` + + 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): """Shorthand for ``Shape(width, signed=False)``.""" return Shape(width, signed=False) @@ -1479,6 +1525,40 @@ def wrapper_memoized(self, *args, **kwargs): 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 ` + - :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 @final class Sample(Value): diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py index 697566c0b..e1c35db1a 100644 --- a/amaranth/lib/data.py +++ b/amaranth/lib/data.py @@ -26,7 +26,7 @@ class Field: Attributes ---------- - shape : :ref:`shape-castable ` + shape : :ref:`shape-like ` 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. @@ -56,7 +56,7 @@ def width(self): """Width of the field. This property should be used over ``self.shape.width`` because ``self.shape`` can be - an arbitrary :ref:`shape-castable ` object, which may not have + an arbitrary :ref:`shape-like ` object, which may not have a ``width`` property. Returns @@ -82,7 +82,7 @@ def __repr__(self): class Layout(ShapeCastable, metaclass=ABCMeta): """Description of a data layout. - The :ref:`shape-castable ` :class:`Layout` interface associates keys + The :ref:`shape-like ` :class:`Layout` interface associates keys (string names or integer indexes) with fields, giving identifiers to spans of bits in an Amaranth value. @@ -96,7 +96,7 @@ class Layout(ShapeCastable, metaclass=ABCMeta): @staticmethod def cast(obj): - """Cast a :ref:`shape-castable ` object to a layout. + """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. @@ -279,7 +279,7 @@ class StructLayout(Layout): Attributes ---------- - members : mapping of :class:`str` to :ref:`shape-castable ` + members : mapping of :class:`str` to :ref:`shape-like ` Dictionary of structure members. """ @@ -350,7 +350,7 @@ class UnionLayout(Layout): Attributes ---------- - members : mapping of :class:`str` to :ref:`shape-castable ` + members : mapping of :class:`str` to :ref:`shape-like ` Dictionary of union members. """ def __init__(self, members): @@ -425,7 +425,7 @@ class ArrayLayout(Layout): Attributes ---------- - elem_shape : :ref:`shape-castable ` + elem_shape : :ref:`shape-like ` Shape of an individual element. length : :class:`int` Amount of elements. @@ -567,7 +567,7 @@ def __repr__(self): class View(ValueCastable): """A value viewed through the lens of a layout. - The :ref:`value-castable ` class :class:`View` provides access to the fields + The :ref:`value-like ` class :class:`View` provides access to the fields of an underlying Amaranth value via the names or indexes defined in the provided layout. 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 :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-castable ` object implementing ``__call__``, it will be + :ref:`shape-like ` object implementing ``__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 @@ -859,7 +859,7 @@ class Struct(View, metaclass=_AggregateMeta): to describe the structure layout and reset values for the fields using Python :term:`variable annotations `. - Any annotations containing :ref:`shape-castable ` objects are used, + Any annotations containing :ref:`shape-like ` objects are used, 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 created by the view. Any other annotations are kept as-is. diff --git a/amaranth/lib/enum.py b/amaranth/lib/enum.py index d8b741643..b15d6d51d 100644 --- a/amaranth/lib/enum.py +++ b/amaranth/lib/enum.py @@ -19,13 +19,13 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta): protocol. This metaclass provides the :meth:`as_shape` method, making its instances - :ref:`shape-castable `, and accepts a ``shape=`` keyword argument + :ref:`shape-like `, and accepts a ``shape=`` keyword argument 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 :meth:`as_shape` is never called, it places no restrictions on the enumeration class or the values of its members. - When a :ref:`value-castable ` is cast to an enum type that is an instance + When a :ref:`value-like ` 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 can be specified by passing the ``view_class=`` keyword argument when creating the enum class. """ @@ -139,7 +139,7 @@ def __call__(cls, value, *args, **kwargs): When given an integer constant, it returns the corresponding enum value, like a standard Python enumeration. - When given a :ref:`value-castable `, it is cast to a value, then wrapped + When given a :ref:`value-like `, it is cast to a value, then wrapped 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 ``view_class`` (like :class:`IntEnum` or :class:`IntFlag`), a plain @@ -214,7 +214,7 @@ class EnumView(ValueCastable): def __init__(self, enum, target): """Constructs a view with the given enum type and target - (a :ref:`value-castable `). + (a :ref:`value-like `). """ 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}") @@ -312,7 +312,7 @@ class FlagView(EnumView): values of the same enum type.""" 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: just like the Python :class:`enum.Flag` class, only bits corresponding to flags actually diff --git a/docs/lang.rst b/docs/lang.rst index 9a874437d..bde2509bb 100644 --- a/docs/lang.rst +++ b/docs/lang.rst @@ -121,14 +121,14 @@ The shape of the constant can be specified explicitly, in which case the number' 0 -.. _lang-shapecasting: +.. _lang-shapelike: 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: @@ -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. -.. _lang-valuecasting: +.. _lang-valuelike: 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 -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 @@ -343,7 +343,7 @@ A *signal* is a value representing a (potentially) varying number. Signals can b Signal shapes ------------- -A signal can be created with an explicitly specified shape (any :ref:`shape-castable ` 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 ` object); if omitted, the shape defaults to ``unsigned(1)``. Although rarely useful, 0-bit signals are permitted. .. doctest:: @@ -444,7 +444,7 @@ Amaranth provides aggregate data structures in the standard library module :mod: Operators ========= -To describe computations, Amaranth values can be combined with each other or with :ref:`value-castable ` 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 ` objects using a rich array of arithmetic, bitwise, logical, bit sequence, and other *operators* to form *expressions*, which are themselves values. .. _lang-abstractexpr: diff --git a/docs/stdlib/data.rst b/docs/stdlib/data.rst index e063550ce..4590b238b 100644 --- a/docs/stdlib/data.rst +++ b/docs/stdlib/data.rst @@ -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) -The :class:`View` is :ref:`value-castable ` 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 ` and can be used anywhere a plain value can be used. For example, it can be assigned to in the usual way: .. testcode:: @@ -135,7 +135,7 @@ In case the data has related operations or transformations, :class:`View` can be def brightness(self): return (self.red + self.green + self.blue)[-8:] -Here, the ``RGBLayout`` class itself is :ref:`shape-castable ` 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 ` 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:: diff --git a/docs/stdlib/enum.rst b/docs/stdlib/enum.rst index 0fa87d957..2267b6038 100644 --- a/docs/stdlib/enum.rst +++ b/docs/stdlib/enum.rst @@ -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``. -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 ` 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 ` can also be explicitly wrapped in a view class by casting it to the enum type: .. doctest:: diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py index 2252a381c..56dfca3d1 100644 --- a/tests/test_hdl_ast.py +++ b/tests/test_hdl_ast.py @@ -1,5 +1,5 @@ import warnings -from enum import Enum +from enum import Enum, EnumMeta from amaranth.hdl.ast import * from amaranth.lib.enum import Enum as AmaranthEnum @@ -189,6 +189,44 @@ def test_recurse(self): 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): def test_cast(self): self.assertIsInstance(Value.cast(0), Const) @@ -1300,6 +1338,50 @@ def test_recurse(self): 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): @_ignore_deprecated def test_const(self):