Skip to content

Commit 7166455

Browse files
committed
lib.data: implement extensibility as specified in RFC 8.
See amaranth-lang/rfcs#8 and #772.
1 parent 68e292c commit 7166455

File tree

3 files changed

+89
-12
lines changed

3 files changed

+89
-12
lines changed

amaranth/lib/data.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ def __eq__(self, other):
187187
return (isinstance(other, Layout) and self.size == other.size and
188188
dict(iter(self)) == dict(iter(other)))
189189

190+
def __call__(self, target):
191+
"""Create a view into a target.
192+
193+
When a :class:`Layout` is used as the shape of a :class:`Field` and accessed through
194+
a :class:`View`, this method is used to wrap the slice of the underlying value into
195+
another view with this layout.
196+
197+
Returns
198+
-------
199+
View
200+
``View(self, target)``
201+
"""
202+
return View(self, target)
203+
190204
def _convert_to_int(self, value):
191205
"""Convert ``value``, which may be a dict or an array of field values, to an integer using
192206
the representation defined by this layout.
@@ -560,10 +574,12 @@ class View(ValueCastable):
560574
################
561575
562576
Slicing a view or accessing its attributes returns a part of the underlying value
563-
corresponding to the field with that index or name, which is always an Amaranth value, but
564-
it could also be a :class:`View` if the shape of the field is a :class:`Layout`, or
565-
an instance of the data class if the shape of the field is a class deriving from
566-
:class:`Struct` or :class:`Union`.
577+
corresponding to the field with that index or name, which is itself either a value or
578+
a value-castable object. If the shape of the field is a :class:`Layout`, it will be
579+
a :class:`View`; if it is a class deriving from :class:`Struct` or :class:`Union`, it
580+
will be an instance of that data class; if it is another
581+
:ref:`shape-castable <lang-shapecasting>` object implementing ``__call__``, it will be
582+
the result of calling that method.
567583
568584
Slicing a view whose layout is an :class:`ArrayLayout` can be done with an index that is
569585
an Amaranth value instead of a constant integer. The returned element is chosen dynamically
@@ -639,9 +655,10 @@ def __getitem__(self, key):
639655
"""Slice the underlying value.
640656
641657
A field corresponding to ``key`` is looked up in the layout. If the field's shape is
642-
a :class:`Layout`, returns a :class:`View`. If it is a subclass of :class:`Struct` or
643-
:class:`Union`, returns an instance of that class. Otherwise, returns an unspecified
644-
Amaranth expression with the right shape.
658+
a shape-castable object that has a ``__call__`` method, it is called and the result is
659+
returned. Otherwise, ``as_shape`` is called repeatedly on the shape until either an object
660+
with a ``__call__`` method is reached, or a ``Shape`` is returned. In the latter case,
661+
returns an unspecified Amaranth expression with the right shape.
645662
646663
Arguments
647664
---------
@@ -650,7 +667,7 @@ def __getitem__(self, key):
650667
651668
Returns
652669
-------
653-
:class:`Value`, inout
670+
:class:`Value` or :class:`ValueCastable`, inout
654671
A slice of the underlying value defined by the field.
655672
656673
Raises
@@ -660,6 +677,8 @@ def __getitem__(self, key):
660677
TypeError
661678
If ``key`` is a value-castable object, but the layout of the view is not
662679
a :class:`ArrayLayout`.
680+
TypeError
681+
If ``ShapeCastable.__call__`` does not return a value or a value-castable object.
663682
"""
664683
if isinstance(self.__layout, ArrayLayout):
665684
shape = self.__layout.elem_shape
@@ -672,10 +691,16 @@ def __getitem__(self, key):
672691
field = self.__layout[key]
673692
shape = field.shape
674693
value = self.__target[field.offset:field.offset + field.width]
675-
if isinstance(shape, _AggregateMeta):
676-
return shape(value)
677-
if isinstance(shape, Layout):
678-
return View(shape, value)
694+
# Field guarantees that the shape-castable object is well-formed, so there is no need
695+
# to handle erroneous cases here.
696+
while isinstance(shape, ShapeCastable):
697+
if hasattr(shape, "__call__"):
698+
value = shape(value)
699+
if not isinstance(value, (Value, ValueCastable)):
700+
raise TypeError("{!r}.__call__() must return a value or "
701+
"a value-castable object, not {!r}"
702+
.format(shape, value))
703+
return value
679704
if Shape.cast(shape).signed:
680705
return value.as_signed()
681706
else:

amaranth/lib/enum.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ def as_shape(cls):
123123
raise TypeError("Enumeration '{}.{}' does not have a defined shape"
124124
.format(cls.__module__, cls.__qualname__))
125125

126+
def __call__(cls, value):
127+
# :class:`py_enum.Enum` uses ``__call__()`` for type casting: ``E(x)`` returns
128+
# the enumeration member whose value equals ``x``. In this case, ``x`` must be a concrete
129+
# value.
130+
# Amaranth extends this to indefinite values, but conceptually the operation is the same:
131+
# :class:`View` calls :meth:`Enum.__call__` to go from a :class:`Value` to something
132+
# representing this enumeration with that value.
133+
# At the moment however, for historical reasons, this is just the value itself. This works
134+
# and is backwards-compatible but is limiting in that it does not allow us to e.g. catch
135+
# comparisons with enum members of the wrong type.
136+
if isinstance(value, Value):
137+
return value
138+
return super().__call__(value)
139+
126140

127141
class Enum(py_enum.Enum, metaclass=EnumMeta):
128142
"""Subclass of the standard :class:`enum.Enum` that has :class:`EnumMeta` as

tests/test_lib_data.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ def test_eq_wrong_recur(self):
364364
sc.shape = sc
365365
self.assertNotEqual(StructLayout({}), sc)
366366

367+
def test_call(self):
368+
sl = StructLayout({"f": unsigned(1)})
369+
s = Signal(1)
370+
v = sl(s)
371+
self.assertIs(Layout.of(v), sl)
372+
self.assertIs(v.as_value(), s)
373+
367374

368375
class ViewTestCase(FHDLTestCase):
369376
def test_construct(self):
@@ -468,6 +475,37 @@ def test_getitem(self):
468475
self.assertRepr(v["t"][0]["u"], "(slice (slice (slice (sig v) 0:4) 0:2) 0:1)")
469476
self.assertRepr(v["t"][1]["v"], "(slice (slice (slice (sig v) 0:4) 2:4) 1:2)")
470477

478+
def test_getitem_custom_call(self):
479+
class Reverser(ShapeCastable):
480+
def as_shape(self):
481+
return unsigned(2)
482+
483+
def __call__(self, value):
484+
return value[::-1]
485+
486+
v = View(StructLayout({
487+
"f": Reverser()
488+
}))
489+
self.assertRepr(v.f, "(cat (slice (slice (sig v) 0:2) 1:2) "
490+
" (slice (slice (sig v) 0:2) 0:1))")
491+
492+
def test_getitem_custom_call_wrong(self):
493+
class WrongCastable(ShapeCastable):
494+
def as_shape(self):
495+
return unsigned(2)
496+
497+
def __call__(self, value):
498+
pass
499+
500+
v = View(StructLayout({
501+
"f": WrongCastable()
502+
}))
503+
with self.assertRaisesRegex(TypeError,
504+
r"^<tests\.test_lib_data\.ViewTestCase\.test_getitem_custom_call_wrong\.<locals>"
505+
r"\.WrongCastable object at 0x.+?>\.__call__\(\) must return a value or "
506+
r"a value-castable object, not None$"):
507+
v.f
508+
471509
def test_index_wrong_missing(self):
472510
with self.assertRaisesRegex(KeyError,
473511
r"^'a'$"):

0 commit comments

Comments
 (0)