Skip to content

Commit 7d99981

Browse files
committed
Implement RFC 15: Lifting shape-castable objects.
See amaranth-lang/rfcs#15 and #784. Note that this RFC breaks the existing syntax for initializing a view with a new signal. Instances of `View(layout)` *must* be changed to `Signal(layout)`.
1 parent e997558 commit 7d99981

File tree

5 files changed

+149
-199
lines changed

5 files changed

+149
-199
lines changed

amaranth/hdl/ast.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABCMeta, abstractmethod
2+
import inspect
23
import warnings
34
import functools
45
from collections import OrderedDict
@@ -45,6 +46,9 @@ def __init_subclass__(cls, **kwargs):
4546
if not hasattr(cls, "as_shape"):
4647
raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
4748
f"the `as_shape` method")
49+
if not (hasattr(cls, "__call__") and inspect.isfunction(cls.__call__)):
50+
raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
51+
f"the `__call__` method")
4852
if not hasattr(cls, "const"):
4953
raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
5054
f"the `const` method")
@@ -949,8 +953,16 @@ def __repr__(self):
949953
return "(repl {!r} {})".format(self.value, self.count)
950954

951955

956+
class _SignalMeta(ABCMeta):
957+
def __call__(cls, shape=None, src_loc_at=0, **kwargs):
958+
signal = super().__call__(shape, **kwargs, src_loc_at=src_loc_at + 1)
959+
if isinstance(shape, ShapeCastable):
960+
return shape(signal)
961+
return signal
962+
963+
952964
# @final
953-
class Signal(Value, DUID):
965+
class Signal(Value, DUID, metaclass=_SignalMeta):
954966
"""A varying integer value.
955967
956968
Parameters

amaranth/lib/data.py

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,13 @@ def size(self):
390390
"""
391391
return max((field.width for field in self._fields.values()), default=0)
392392

393+
def const(self, init):
394+
if init is not None and len(init) > 1:
395+
raise ValueError("Initializer for at most one field can be provided for "
396+
"a union layout (specified: {})"
397+
.format(", ".join(init.keys())))
398+
return super().const(init)
399+
393400
def __repr__(self):
394401
return f"UnionLayout({self.members!r})"
395402

@@ -609,33 +616,21 @@ class View(ValueCastable):
609616
classes provided in this module are subclasses of :class:`View` that also provide a concise way
610617
to define a layout.
611618
"""
612-
def __init__(self, layout, target=None, *, name=None, reset=None, reset_less=None,
613-
attrs=None, decoder=None, src_loc_at=0):
619+
def __init__(self, layout, target):
614620
try:
615621
cast_layout = Layout.cast(layout)
616622
except TypeError as e:
617623
raise TypeError("View layout must be a layout, not {!r}"
618624
.format(layout)) from e
619-
if target is not None:
620-
if (name is not None or reset is not None or reset_less is not None or
621-
attrs is not None or decoder is not None):
622-
raise ValueError("View target cannot be provided at the same time as any of "
623-
"the Signal constructor arguments (name, reset, reset_less, "
624-
"attrs, decoder)")
625-
try:
626-
cast_target = Value.cast(target)
627-
except TypeError as e:
628-
raise TypeError("View target must be a value-castable object, not {!r}"
629-
.format(target)) from e
630-
if len(cast_target) != cast_layout.size:
631-
raise ValueError("View target is {} bit(s) wide, which is not compatible with "
632-
"the {} bit(s) wide view layout"
633-
.format(len(cast_target), cast_layout.size))
634-
else:
635-
if reset_less is None:
636-
reset_less = False
637-
cast_target = Signal(layout, name=name, reset=reset, reset_less=reset_less,
638-
attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
625+
try:
626+
cast_target = Value.cast(target)
627+
except TypeError as e:
628+
raise TypeError("View target must be a value-castable object, not {!r}"
629+
.format(target)) from e
630+
if len(cast_target) != cast_layout.size:
631+
raise ValueError("View target is {} bit(s) wide, which is not compatible with "
632+
"the {} bit(s) wide view layout"
633+
.format(len(cast_target), cast_layout.size))
639634
self.__orig_layout = layout
640635
self.__layout = cast_layout
641636
self.__target = cast_target
@@ -752,8 +747,8 @@ def __new__(metacls, name, bases, namespace):
752747
elif all(not hasattr(base, "_AggregateMeta__layout") for base in bases):
753748
# This is a leaf class with its own layout. It is shape-castable and can
754749
# be instantiated. It can also be subclassed, and used to share layout and behavior.
755-
layout = dict()
756-
reset = dict()
750+
layout = dict()
751+
default = dict()
757752
for name in {**namespace["__annotations__"]}:
758753
try:
759754
Shape.cast(namespace["__annotations__"][name])
@@ -762,15 +757,15 @@ def __new__(metacls, name, bases, namespace):
762757
continue
763758
layout[name] = namespace["__annotations__"].pop(name)
764759
if name in namespace:
765-
reset[name] = namespace.pop(name)
760+
default[name] = namespace.pop(name)
766761
cls = type.__new__(metacls, name, bases, namespace)
767762
if cls.__layout_cls is UnionLayout:
768-
if len(reset) > 1:
763+
if len(default) > 1:
769764
raise ValueError("Reset value for at most one field can be provided for "
770765
"a union class (specified: {})"
771-
.format(", ".join(reset.keys())))
772-
cls.__layout = cls.__layout_cls(layout)
773-
cls.__reset = reset
766+
.format(", ".join(default.keys())))
767+
cls.__layout = cls.__layout_cls(layout)
768+
cls.__default = default
774769
return cls
775770
else:
776771
# This is a class that has a base class with a layout and annotations. Such a class
@@ -785,28 +780,24 @@ def as_shape(cls):
785780
.format(cls.__module__, cls.__qualname__))
786781
return cls.__layout
787782

788-
def const(cls, init):
789-
return cls.as_shape().const(init)
790-
783+
def __call__(cls, target):
784+
# This method exists to pass the override check done by ShapeCastable.
785+
return super().__call__(cls, target)
791786

792-
class _Aggregate(View, metaclass=_AggregateMeta):
793-
def __init__(self, target=None, *, name=None, reset=None, reset_less=None,
794-
attrs=None, decoder=None, src_loc_at=0):
795-
if self.__class__._AggregateMeta__layout_cls is UnionLayout:
796-
if reset is not None and len(reset) > 1:
797-
raise ValueError("Reset value for at most one field can be provided for "
787+
def const(cls, init):
788+
if cls.__layout_cls is UnionLayout:
789+
if init is not None and len(init) > 1:
790+
raise ValueError("Initializer for at most one field can be provided for "
798791
"a union class (specified: {})"
799-
.format(", ".join(reset.keys())))
800-
if target is None and hasattr(self.__class__, "_AggregateMeta__reset"):
801-
if reset is None:
802-
reset = self.__class__._AggregateMeta__reset
803-
elif self.__class__._AggregateMeta__layout_cls is not UnionLayout:
804-
reset = {**self.__class__._AggregateMeta__reset, **reset}
805-
super().__init__(self.__class__, target, name=name, reset=reset, reset_less=reset_less,
806-
attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
792+
.format(", ".join(init.keys())))
793+
return cls.as_shape().const(init or cls.__default)
794+
else:
795+
fields = cls.__default.copy()
796+
fields.update(init or {})
797+
return cls.as_shape().const(fields)
807798

808799

809-
class Struct(_Aggregate):
800+
class Struct(View, metaclass=_AggregateMeta):
810801
"""Structures defined with annotations.
811802
812803
The :class:`Struct` base class is a subclass of :class:`View` that provides a concise way
@@ -842,14 +833,14 @@ def is_subnormal(self):
842833
843834
>>> IEEE754Single.as_shape()
844835
StructLayout({'fraction': 23, 'exponent': 8, 'sign': 1})
845-
>>> Signal(IEEE754Single).width
836+
>>> Signal(IEEE754Single).as_value().width
846837
32
847838
848839
Instances of this class can be used where :ref:`values <lang-values>` are expected:
849840
850841
.. doctest::
851842
852-
>>> flt = IEEE754Single()
843+
>>> flt = Signal(IEEE754Single)
853844
>>> Signal(32).eq(flt)
854845
(eq (sig $signal) (sig flt))
855846
@@ -866,11 +857,11 @@ def is_subnormal(self):
866857
867858
.. doctest::
868859
869-
>>> hex(IEEE754Single().as_value().reset)
860+
>>> hex(Signal(IEEE754Single).as_value().reset)
870861
'0x3f800000'
871-
>>> hex(IEEE754Single(reset={'sign': 1}).as_value().reset)
862+
>>> hex(Signal(IEEE754Single, reset={'sign': 1}).as_value().reset)
872863
'0xbf800000'
873-
>>> hex(IEEE754Single(reset={'exponent': 0}).as_value().reset)
864+
>>> hex(Signal(IEEE754Single, reset={'exponent': 0}).as_value().reset)
874865
'0x0'
875866
876867
Classes inheriting from :class:`Struct` can be used as base classes. The only restrictions
@@ -903,15 +894,15 @@ class HeaderWithParam(HasChecksum):
903894
Traceback (most recent call last):
904895
...
905896
TypeError: Aggregate class 'HasChecksum' does not have a defined shape
906-
>>> bare = BareHeader(); bare.checksum()
897+
>>> bare = Signal(BareHeader); bare.checksum()
907898
(+ (+ (+ (const 1'd0) (slice (sig bare) 0:8)) (slice (sig bare) 8:16)) (slice (sig bare) 16:24))
908-
>>> param = HeaderWithParam(); param.checksum()
899+
>>> param = Signal(HeaderWithParam); param.checksum()
909900
(+ (+ (+ (+ (const 1'd0) (slice (sig param) 0:8)) (slice (sig param) 8:16)) (slice (sig param) 16:24)) (slice (sig param) 24:32))
910901
"""
911902
_AggregateMeta__layout_cls = StructLayout
912903

913904

914-
class Union(_Aggregate):
905+
class Union(View, metaclass=_AggregateMeta):
915906
"""Unions defined with annotations.
916907
917908
The :class:`Union` base class is a subclass of :class:`View` that provides a concise way
@@ -931,9 +922,9 @@ class VarInt(Union):
931922
932923
.. doctest::
933924
934-
>>> VarInt().as_value().reset
925+
>>> Signal(VarInt).as_value().reset
935926
256
936-
>>> VarInt(reset={'int8': 10}).as_value().reset
927+
>>> Signal(VarInt, reset={'int8': 10}).as_value().reset
937928
10
938929
"""
939930
_AggregateMeta__layout_cls = UnionLayout

docs/stdlib/data.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ While this implementation works, it is repetitive, error-prone, hard to read, an
5656
"blue": 5
5757
})
5858

59-
i_color = data.View(rgb565_layout)
59+
i_color = Signal(rgb565_layout)
6060
o_gray = Signal(8)
6161

6262
m.d.comb += o_gray.eq((i_color.red + i_color.green + i_color.blue) << 1)
@@ -82,7 +82,7 @@ For example, consider a module that processes RGB pixels in groups of up to four
8282
"valid": 4
8383
})
8484

85-
i_stream = data.View(input_layout)
85+
i_stream = Signal(input_layout)
8686
r_accum = Signal(32)
8787

8888
m.d.sync += r_accum.eq(
@@ -120,7 +120,7 @@ In case the data has related operations or transformations, :class:`View` can be
120120

121121
.. testcode::
122122

123-
class RGBPixelLayout(data.StructLayout):
123+
class RGBLayout(data.StructLayout):
124124
def __init__(self, r_bits, g_bits, b_bits):
125125
super().__init__({
126126
"red": unsigned(r_bits),
@@ -129,20 +129,20 @@ In case the data has related operations or transformations, :class:`View` can be
129129
})
130130

131131
def __call__(self, value):
132-
return RGBPixelView(self, value)
132+
return RGBView(self, value)
133133

134-
class RGBPixelView(data.View):
134+
class RGBView(data.View):
135135
def brightness(self):
136136
return (self.red + self.green + self.blue)[-8:]
137137

138-
Here, the ``RGBLayout`` class itself is :ref:`shape-castable <lang-shapecasting>` and can be used anywhere a shape is accepted:
138+
Here, the ``RGBLayout`` class itself is :ref:`shape-castable <lang-shapecasting>` 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``:
139139

140140
.. doctest::
141141

142-
>>> pixel = Signal(RGBPixelLayout(5, 6, 5))
143-
>>> len(pixel)
142+
>>> pixel = Signal(RGBLayout(5, 6, 5))
143+
>>> len(pixel.as_value())
144144
16
145-
>>> RGBPixelView(RGBPixelLayout(5, 6, 5), pixel).red
145+
>>> pixel.red
146146
(slice (sig pixel) 0:5)
147147

148148
In case the data format is static, :class:`Struct` (or :class:`Union`) can be subclassed instead of :class:`View`, to reduce the amount of boilerplate needed:
@@ -189,7 +189,7 @@ One module could submit a command with:
189189

190190
.. testcode::
191191

192-
cmd = Command()
192+
cmd = Signal(Command)
193193

194194
m.d.comb += [
195195
cmd.valid.eq(1),

tests/test_hdl_ast.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,11 @@ def __init__(self, dest):
145145
def as_shape(self):
146146
return self.dest
147147

148-
def const(self, obj):
149-
return Const(obj, self.dest)
148+
def __call__(self, value):
149+
return value
150+
151+
def const(self, init):
152+
return Const(init, self.dest)
150153

151154

152155
class ShapeCastableTestCase(FHDLTestCase):
@@ -1004,6 +1007,9 @@ class CastableFromHex(ShapeCastable):
10041007
def as_shape(self):
10051008
return unsigned(8)
10061009

1010+
def __call__(self, value):
1011+
return value
1012+
10071013
def const(self, init):
10081014
return int(init, 16)
10091015

0 commit comments

Comments
 (0)