diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py index 7772d71..d99958b 100644 --- a/amaranth/hdl/ast.py +++ b/amaranth/hdl/ast.py @@ -135,16 +135,22 @@ class Value(metaclass=ABCMeta): Booleans and integers are wrapped into a :class:`Const`. Enumerations whose members are all integers are converted to a :class:`Const` with a shape that fits every member. + :class:`ValueCastable` objects are recursively cast to an Amaranth value. """ - if isinstance(obj, Value): - return obj - if isinstance(obj, int): - return Const(obj) - if isinstance(obj, Enum): - return Const(obj.value, Shape.cast(type(obj))) - if isinstance(obj, ValueCastable): - return obj.as_value() - raise TypeError("Object {!r} cannot be converted to an Amaranth value".format(obj)) + while True: + if isinstance(obj, Value): + return obj + elif isinstance(obj, int): + return Const(obj) + elif isinstance(obj, Enum): + return Const(obj.value, Shape.cast(type(obj))) + elif isinstance(obj, ValueCastable): + new_obj = obj.as_value() + else: + raise TypeError("Object {!r} cannot be converted to an Amaranth value".format(obj)) + if new_obj is obj: + raise RecursionError("Value-castable object {!r} casts to itself".format(obj)) + obj = new_obj def __init__(self, *, src_loc_at=0): super().__init__() @@ -1276,7 +1282,7 @@ class UserValue(Value): class ValueCastable: - """Base class for classes which can be cast to Values. + """Interface of objects can be cast to :class:`Value`s. A ``ValueCastable`` can be cast to ``Value``, meaning its precise representation does not have to be immediately known. This is useful in certain metaprogramming scenarios. Instead of diff --git a/docs/lang.rst b/docs/lang.rst index 20b0818..0c2a47e 100644 --- a/docs/lang.rst +++ b/docs/lang.rst @@ -220,7 +220,7 @@ Value casting Like shapes, values may be *cast* from other objects, which are called *value-castable*. Casting allows objects that are not provided by Amaranth, such as integers or enumeration members, to be used in Amaranth expressions directly. -.. TODO: link to UserValue +.. 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. diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py index d116e78..d0945e1 100644 --- a/tests/test_hdl_ast.py +++ b/tests/test_hdl_ast.py @@ -1060,6 +1060,15 @@ class UserValueTestCase(FHDLTestCase): self.assertEqual(uv.lower_count, 1) +class MockValueCastable(ValueCastable): + def __init__(self, dest): + self.dest = dest + + @ValueCastable.lowermethod + def as_value(self): + return self.dest + + class MockValueCastableChanges(ValueCastable): def __init__(self, width=0): self.width = width @@ -1097,14 +1106,14 @@ class MockValueCastableCustomGetattr(ValueCastable): class ValueCastableTestCase(FHDLTestCase): def test_not_decorated(self): with self.assertRaisesRegex(TypeError, - r"^Class 'MockValueCastableNotDecorated' deriving from `ValueCastable` must decorate the `as_value` " - r"method with the `ValueCastable.lowermethod` decorator$"): + r"^Class 'MockValueCastableNotDecorated' deriving from `ValueCastable` must " + r"decorate the `as_value` method with the `ValueCastable.lowermethod` decorator$"): vc = MockValueCastableNotDecorated() def test_no_override(self): with self.assertRaisesRegex(TypeError, - r"^Class 'MockValueCastableNoOverride' deriving from `ValueCastable` must override the `as_value` " - r"method$"): + r"^Class 'MockValueCastableNoOverride' deriving from `ValueCastable` must " + r"override the `as_value` method$"): vc = MockValueCastableNoOverride() def test_memoized(self): @@ -1121,6 +1130,17 @@ class ValueCastableTestCase(FHDLTestCase): vc = MockValueCastableCustomGetattr() vc.as_value() # shouldn't call __getattr__ + def test_recurse_bad(self): + vc = MockValueCastable(None) + vc.dest = vc + with self.assertRaisesRegex(RecursionError, + r"^Value-castable object <.+> casts to itself$"): + Value.cast(vc) + + def test_recurse(self): + vc = MockValueCastable(MockValueCastable(Signal())) + self.assertIsInstance(Value.cast(vc), Signal) + class SampleTestCase(FHDLTestCase): def test_const(self):