Skip to content

Commit 0723f6b

Browse files
committed
hdl.ast: recursively cast ValueCastable objects to values.
1 parent 3b79948 commit 0723f6b

File tree

3 files changed

+41
-15
lines changed

3 files changed

+41
-15
lines changed

amaranth/hdl/ast.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,22 @@ def cast(obj):
135135
136136
Booleans and integers are wrapped into a :class:`Const`. Enumerations whose members are
137137
all integers are converted to a :class:`Const` with a shape that fits every member.
138+
:class:`ValueCastable` objects are recursively cast to an Amaranth value.
138139
"""
139-
if isinstance(obj, Value):
140-
return obj
141-
if isinstance(obj, int):
142-
return Const(obj)
143-
if isinstance(obj, Enum):
144-
return Const(obj.value, Shape.cast(type(obj)))
145-
if isinstance(obj, ValueCastable):
146-
return obj.as_value()
147-
raise TypeError("Object {!r} cannot be converted to an Amaranth value".format(obj))
140+
while True:
141+
if isinstance(obj, Value):
142+
return obj
143+
elif isinstance(obj, int):
144+
return Const(obj)
145+
elif isinstance(obj, Enum):
146+
return Const(obj.value, Shape.cast(type(obj)))
147+
elif isinstance(obj, ValueCastable):
148+
new_obj = obj.as_value()
149+
else:
150+
raise TypeError("Object {!r} cannot be converted to an Amaranth value".format(obj))
151+
if new_obj is obj:
152+
raise RecursionError("Value-castable object {!r} casts to itself".format(obj))
153+
obj = new_obj
148154

149155
def __init__(self, *, src_loc_at=0):
150156
super().__init__()
@@ -1276,7 +1282,7 @@ def _rhs_signals(self):
12761282

12771283

12781284
class ValueCastable:
1279-
"""Base class for classes which can be cast to Values.
1285+
"""Interface of objects can be cast to :class:`Value`s.
12801286
12811287
A ``ValueCastable`` can be cast to ``Value``, meaning its precise representation does not have
12821288
to be immediately known. This is useful in certain metaprogramming scenarios. Instead of

docs/lang.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ Value casting
220220

221221
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.
222222

223-
.. TODO: link to UserValue
223+
.. TODO: link to ValueCastable
224224
225225
Casting to a value can be done explicitly with ``Value.cast``, but is usually implicit, since value-castable objects are accepted anywhere values are.
226226

tests/test_hdl_ast.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,15 @@ def test_lower_to_user_value(self):
10601060
self.assertEqual(uv.lower_count, 1)
10611061

10621062

1063+
class MockValueCastable(ValueCastable):
1064+
def __init__(self, dest):
1065+
self.dest = dest
1066+
1067+
@ValueCastable.lowermethod
1068+
def as_value(self):
1069+
return self.dest
1070+
1071+
10631072
class MockValueCastableChanges(ValueCastable):
10641073
def __init__(self, width=0):
10651074
self.width = width
@@ -1097,14 +1106,14 @@ def __getattr__(self, attr):
10971106
class ValueCastableTestCase(FHDLTestCase):
10981107
def test_not_decorated(self):
10991108
with self.assertRaisesRegex(TypeError,
1100-
r"^Class 'MockValueCastableNotDecorated' deriving from `ValueCastable` must decorate the `as_value` "
1101-
r"method with the `ValueCastable.lowermethod` decorator$"):
1109+
r"^Class 'MockValueCastableNotDecorated' deriving from `ValueCastable` must "
1110+
r"decorate the `as_value` method with the `ValueCastable.lowermethod` decorator$"):
11021111
vc = MockValueCastableNotDecorated()
11031112

11041113
def test_no_override(self):
11051114
with self.assertRaisesRegex(TypeError,
1106-
r"^Class 'MockValueCastableNoOverride' deriving from `ValueCastable` must override the `as_value` "
1107-
r"method$"):
1115+
r"^Class 'MockValueCastableNoOverride' deriving from `ValueCastable` must "
1116+
r"override the `as_value` method$"):
11081117
vc = MockValueCastableNoOverride()
11091118

11101119
def test_memoized(self):
@@ -1121,6 +1130,17 @@ def test_custom_getattr(self):
11211130
vc = MockValueCastableCustomGetattr()
11221131
vc.as_value() # shouldn't call __getattr__
11231132

1133+
def test_recurse_bad(self):
1134+
vc = MockValueCastable(None)
1135+
vc.dest = vc
1136+
with self.assertRaisesRegex(RecursionError,
1137+
r"^Value-castable object <.+> casts to itself$"):
1138+
Value.cast(vc)
1139+
1140+
def test_recurse(self):
1141+
vc = MockValueCastable(MockValueCastable(Signal()))
1142+
self.assertIsInstance(Value.cast(vc), Signal)
1143+
11241144

11251145
class SampleTestCase(FHDLTestCase):
11261146
def test_const(self):

0 commit comments

Comments
 (0)