lib.data: implement extensibility as specified in RFC 8.
See amaranth-lang/rfcs#8 and #772.
This commit is contained in:
parent
68e292c681
commit
7166455a6a
|
@ -187,6 +187,20 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
|
||||||
return (isinstance(other, Layout) and self.size == other.size and
|
return (isinstance(other, Layout) and self.size == other.size and
|
||||||
dict(iter(self)) == dict(iter(other)))
|
dict(iter(self)) == dict(iter(other)))
|
||||||
|
|
||||||
|
def __call__(self, target):
|
||||||
|
"""Create a view into a target.
|
||||||
|
|
||||||
|
When a :class:`Layout` is used as the shape of a :class:`Field` and accessed through
|
||||||
|
a :class:`View`, this method is used to wrap the slice of the underlying value into
|
||||||
|
another view with this layout.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
View
|
||||||
|
``View(self, target)``
|
||||||
|
"""
|
||||||
|
return View(self, target)
|
||||||
|
|
||||||
def _convert_to_int(self, value):
|
def _convert_to_int(self, value):
|
||||||
"""Convert ``value``, which may be a dict or an array of field values, to an integer using
|
"""Convert ``value``, which may be a dict or an array of field values, to an integer using
|
||||||
the representation defined by this layout.
|
the representation defined by this layout.
|
||||||
|
@ -560,10 +574,12 @@ class View(ValueCastable):
|
||||||
################
|
################
|
||||||
|
|
||||||
Slicing a view or accessing its attributes returns a part of the underlying value
|
Slicing a view or accessing its attributes returns a part of the underlying value
|
||||||
corresponding to the field with that index or name, which is always an Amaranth value, but
|
corresponding to the field with that index or name, which is itself either a value or
|
||||||
it could also be a :class:`View` if the shape of the field is a :class:`Layout`, or
|
a value-castable object. If the shape of the field is a :class:`Layout`, it will be
|
||||||
an instance of the data class if the shape of the field is a class deriving from
|
a :class:`View`; if it is a class deriving from :class:`Struct` or :class:`Union`, it
|
||||||
:class:`Struct` or :class:`Union`.
|
will be an instance of that data class; if it is another
|
||||||
|
:ref:`shape-castable <lang-shapecasting>` 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
|
Slicing a view whose layout is an :class:`ArrayLayout` can be done with an index that is
|
||||||
an Amaranth value instead of a constant integer. The returned element is chosen dynamically
|
an Amaranth value instead of a constant integer. The returned element is chosen dynamically
|
||||||
|
@ -639,9 +655,10 @@ class View(ValueCastable):
|
||||||
"""Slice the underlying value.
|
"""Slice the underlying value.
|
||||||
|
|
||||||
A field corresponding to ``key`` is looked up in the layout. If the field's shape is
|
A field corresponding to ``key`` is looked up in the layout. If the field's shape is
|
||||||
a :class:`Layout`, returns a :class:`View`. If it is a subclass of :class:`Struct` or
|
a shape-castable object that has a ``__call__`` method, it is called and the result is
|
||||||
:class:`Union`, returns an instance of that class. Otherwise, returns an unspecified
|
returned. Otherwise, ``as_shape`` is called repeatedly on the shape until either an object
|
||||||
Amaranth expression with the right shape.
|
with a ``__call__`` method is reached, or a ``Shape`` is returned. In the latter case,
|
||||||
|
returns an unspecified Amaranth expression with the right shape.
|
||||||
|
|
||||||
Arguments
|
Arguments
|
||||||
---------
|
---------
|
||||||
|
@ -650,7 +667,7 @@ class View(ValueCastable):
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
:class:`Value`, inout
|
:class:`Value` or :class:`ValueCastable`, inout
|
||||||
A slice of the underlying value defined by the field.
|
A slice of the underlying value defined by the field.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
|
@ -660,6 +677,8 @@ class View(ValueCastable):
|
||||||
TypeError
|
TypeError
|
||||||
If ``key`` is a value-castable object, but the layout of the view is not
|
If ``key`` is a value-castable object, but the layout of the view is not
|
||||||
a :class:`ArrayLayout`.
|
a :class:`ArrayLayout`.
|
||||||
|
TypeError
|
||||||
|
If ``ShapeCastable.__call__`` does not return a value or a value-castable object.
|
||||||
"""
|
"""
|
||||||
if isinstance(self.__layout, ArrayLayout):
|
if isinstance(self.__layout, ArrayLayout):
|
||||||
shape = self.__layout.elem_shape
|
shape = self.__layout.elem_shape
|
||||||
|
@ -672,10 +691,16 @@ class View(ValueCastable):
|
||||||
field = self.__layout[key]
|
field = self.__layout[key]
|
||||||
shape = field.shape
|
shape = field.shape
|
||||||
value = self.__target[field.offset:field.offset + field.width]
|
value = self.__target[field.offset:field.offset + field.width]
|
||||||
if isinstance(shape, _AggregateMeta):
|
# Field guarantees that the shape-castable object is well-formed, so there is no need
|
||||||
return shape(value)
|
# to handle erroneous cases here.
|
||||||
if isinstance(shape, Layout):
|
while isinstance(shape, ShapeCastable):
|
||||||
return View(shape, value)
|
if hasattr(shape, "__call__"):
|
||||||
|
value = shape(value)
|
||||||
|
if not isinstance(value, (Value, ValueCastable)):
|
||||||
|
raise TypeError("{!r}.__call__() must return a value or "
|
||||||
|
"a value-castable object, not {!r}"
|
||||||
|
.format(shape, value))
|
||||||
|
return value
|
||||||
if Shape.cast(shape).signed:
|
if Shape.cast(shape).signed:
|
||||||
return value.as_signed()
|
return value.as_signed()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -123,6 +123,20 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta):
|
||||||
raise TypeError("Enumeration '{}.{}' does not have a defined shape"
|
raise TypeError("Enumeration '{}.{}' does not have a defined shape"
|
||||||
.format(cls.__module__, cls.__qualname__))
|
.format(cls.__module__, cls.__qualname__))
|
||||||
|
|
||||||
|
def __call__(cls, value):
|
||||||
|
# :class:`py_enum.Enum` uses ``__call__()`` for type casting: ``E(x)`` returns
|
||||||
|
# the enumeration member whose value equals ``x``. In this case, ``x`` must be a concrete
|
||||||
|
# value.
|
||||||
|
# Amaranth extends this to indefinite values, but conceptually the operation is the same:
|
||||||
|
# :class:`View` calls :meth:`Enum.__call__` to go from a :class:`Value` to something
|
||||||
|
# representing this enumeration with that value.
|
||||||
|
# At the moment however, for historical reasons, this is just the value itself. This works
|
||||||
|
# and is backwards-compatible but is limiting in that it does not allow us to e.g. catch
|
||||||
|
# comparisons with enum members of the wrong type.
|
||||||
|
if isinstance(value, Value):
|
||||||
|
return value
|
||||||
|
return super().__call__(value)
|
||||||
|
|
||||||
|
|
||||||
class Enum(py_enum.Enum, metaclass=EnumMeta):
|
class Enum(py_enum.Enum, metaclass=EnumMeta):
|
||||||
"""Subclass of the standard :class:`enum.Enum` that has :class:`EnumMeta` as
|
"""Subclass of the standard :class:`enum.Enum` that has :class:`EnumMeta` as
|
||||||
|
|
|
@ -364,6 +364,13 @@ class LayoutTestCase(TestCase):
|
||||||
sc.shape = sc
|
sc.shape = sc
|
||||||
self.assertNotEqual(StructLayout({}), sc)
|
self.assertNotEqual(StructLayout({}), sc)
|
||||||
|
|
||||||
|
def test_call(self):
|
||||||
|
sl = StructLayout({"f": unsigned(1)})
|
||||||
|
s = Signal(1)
|
||||||
|
v = sl(s)
|
||||||
|
self.assertIs(Layout.of(v), sl)
|
||||||
|
self.assertIs(v.as_value(), s)
|
||||||
|
|
||||||
|
|
||||||
class ViewTestCase(FHDLTestCase):
|
class ViewTestCase(FHDLTestCase):
|
||||||
def test_construct(self):
|
def test_construct(self):
|
||||||
|
@ -468,6 +475,37 @@ class ViewTestCase(FHDLTestCase):
|
||||||
self.assertRepr(v["t"][0]["u"], "(slice (slice (slice (sig v) 0:4) 0:2) 0:1)")
|
self.assertRepr(v["t"][0]["u"], "(slice (slice (slice (sig v) 0:4) 0:2) 0:1)")
|
||||||
self.assertRepr(v["t"][1]["v"], "(slice (slice (slice (sig v) 0:4) 2:4) 1:2)")
|
self.assertRepr(v["t"][1]["v"], "(slice (slice (slice (sig v) 0:4) 2:4) 1:2)")
|
||||||
|
|
||||||
|
def test_getitem_custom_call(self):
|
||||||
|
class Reverser(ShapeCastable):
|
||||||
|
def as_shape(self):
|
||||||
|
return unsigned(2)
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
return value[::-1]
|
||||||
|
|
||||||
|
v = View(StructLayout({
|
||||||
|
"f": Reverser()
|
||||||
|
}))
|
||||||
|
self.assertRepr(v.f, "(cat (slice (slice (sig v) 0:2) 1:2) "
|
||||||
|
" (slice (slice (sig v) 0:2) 0:1))")
|
||||||
|
|
||||||
|
def test_getitem_custom_call_wrong(self):
|
||||||
|
class WrongCastable(ShapeCastable):
|
||||||
|
def as_shape(self):
|
||||||
|
return unsigned(2)
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
v = View(StructLayout({
|
||||||
|
"f": WrongCastable()
|
||||||
|
}))
|
||||||
|
with self.assertRaisesRegex(TypeError,
|
||||||
|
r"^<tests\.test_lib_data\.ViewTestCase\.test_getitem_custom_call_wrong\.<locals>"
|
||||||
|
r"\.WrongCastable object at 0x.+?>\.__call__\(\) must return a value or "
|
||||||
|
r"a value-castable object, not None$"):
|
||||||
|
v.f
|
||||||
|
|
||||||
def test_index_wrong_missing(self):
|
def test_index_wrong_missing(self):
|
||||||
with self.assertRaisesRegex(KeyError,
|
with self.assertRaisesRegex(KeyError,
|
||||||
r"^'a'$"):
|
r"^'a'$"):
|
||||||
|
|
Loading…
Reference in a new issue