Skip to content

Commit c7ef05c

Browse files
committed
lib.data: improve annotation handling for Struct and Union.
* Annotations like `s: unsigned(4) = 1` are recognized and the assigned value is used as the reset value for the implicitly created `Signal`. * Base classes inheriting from `Struct` and `Union` without specifying a layout are recognized. * Classes that both inherit from a base class with a layout and specify a layout are rejected.
1 parent 0ee5de0 commit c7ef05c

File tree

2 files changed

+99
-12
lines changed

2 files changed

+99
-12
lines changed

amaranth/lib/data.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ def __init__(self, layout, target=None, *, name=None, reset=None, reset_less=Non
323323
try:
324324
cast_layout = Layout.cast(layout)
325325
except TypeError as e:
326-
raise TypeError("View layout must be a Layout instance, not {!r}"
326+
raise TypeError("View layout must be a layout, not {!r}"
327327
.format(layout)) from e
328328
if target is not None:
329329
if (name is not None or reset is not None or reset_less is not None or
@@ -398,28 +398,51 @@ def __getattr__(self, name):
398398

399399

400400
class _AggregateMeta(ShapeCastable, type):
401-
def __new__(metacls, name, bases, namespace, *, _layout_cls=None, **kwargs):
402-
cls = type.__new__(metacls, name, bases, namespace, **kwargs)
403-
if _layout_cls is not None:
404-
cls.__layout_cls = _layout_cls
405-
if "__annotations__" in namespace:
401+
def __new__(metacls, name, bases, namespace):
402+
if "__annotations__" not in namespace:
403+
# This is a base class without its own layout. It is not shape-castable, and cannot
404+
# be instantiated. It can be used to share behavior.
405+
return type.__new__(metacls, name, bases, namespace)
406+
elif all(not hasattr(base, "_AggregateMeta__layout") for base in bases):
407+
# This is a leaf class with its own layout. It is shape-castable and can
408+
# be instantiated. It can also be subclassed, and used to share layout and behavior.
409+
reset = dict()
410+
for name in namespace["__annotations__"]:
411+
if name in namespace:
412+
reset[name] = namespace.pop(name)
413+
cls = type.__new__(metacls, name, bases, namespace)
406414
cls.__layout = cls.__layout_cls(namespace["__annotations__"])
407-
return cls
415+
cls.__reset = reset
416+
return cls
417+
else:
418+
# This is a class that has a base class with a layout and annotations. Such a class
419+
# is not well-formed.
420+
raise TypeError("Aggregate class '{}' must either inherits or specify a layout, "
421+
"not both"
422+
.format(name))
408423

409424
def as_shape(cls):
425+
if not hasattr(cls, "_AggregateMeta__layout"):
426+
raise TypeError("Aggregate class '{}.{}' does not have a defined shape"
427+
.format(cls.__module__, cls.__qualname__))
410428
return cls.__layout
411429

412430

413431
class _Aggregate(View, metaclass=_AggregateMeta):
414432
def __init__(self, target=None, *, name=None, reset=None, reset_less=None,
415433
attrs=None, decoder=None, src_loc_at=0):
434+
if target is None and hasattr(self.__class__, "_AggregateMeta__reset"):
435+
if reset is None:
436+
reset = self.__class__._AggregateMeta__reset
437+
else:
438+
reset = {**self.__class__._AggregateMeta__reset, **reset}
416439
super().__init__(self.__class__, target, name=name, reset=reset, reset_less=reset_less,
417440
attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
418441

419442

420-
class Struct(_Aggregate, _layout_cls=StructLayout):
421-
pass
443+
class Struct(_Aggregate):
444+
_AggregateMeta__layout_cls = StructLayout
422445

423446

424-
class Union(_Aggregate, _layout_cls=UnionLayout):
425-
pass
447+
class Union(_Aggregate):
448+
_AggregateMeta__layout_cls = UnionLayout

tests/test_lib_data.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ def test_construct_signal_decoder(self):
411411

412412
def test_layout_wrong(self):
413413
with self.assertRaisesRegex(TypeError,
414-
r"^View layout must be a Layout instance, not <.+?>$"):
414+
r"^View layout must be a layout, not <.+?>$"):
415415
View(object(), Signal(1))
416416

417417
def test_target_wrong_type(self):
@@ -575,6 +575,70 @@ def test_construct_signal_kwargs(self):
575575
self.assertEqual(s.attrs, {"debug": 1})
576576
self.assertEqual(s.decoder, decoder)
577577

578+
def test_construct_reset(self):
579+
class S(Struct):
580+
p: 4
581+
q: 2 = 1
582+
583+
with self.assertRaises(AttributeError):
584+
S.q
585+
586+
v1 = S()
587+
self.assertEqual(v1.as_value().reset, 0b010000)
588+
v2 = S(reset=dict(p=0b0011))
589+
self.assertEqual(v2.as_value().reset, 0b010011)
590+
v3 = S(reset=dict(p=0b0011, q=0b00))
591+
self.assertEqual(v3.as_value().reset, 0b000011)
592+
593+
def test_shape_undefined_wrong(self):
594+
class S(Struct):
595+
pass
596+
597+
with self.assertRaisesRegex(TypeError,
598+
r"^Aggregate class '.+?\.S' does not have a defined shape$"):
599+
Shape.cast(S)
600+
601+
def test_base_class_1(self):
602+
class Sb(Struct):
603+
def add(self):
604+
return self.a + self.b
605+
606+
class Sb1(Sb):
607+
a: 1
608+
b: 1
609+
610+
class Sb2(Sb):
611+
a: 2
612+
b: 2
613+
614+
self.assertEqual(Sb1().add().shape(), unsigned(2))
615+
self.assertEqual(Sb2().add().shape(), unsigned(3))
616+
617+
def test_base_class_2(self):
618+
class Sb(Struct):
619+
a: 2
620+
b: 2
621+
622+
class Sb1(Sb):
623+
def do(self):
624+
return Cat(self.a, self.b)
625+
626+
class Sb2(Sb):
627+
def do(self):
628+
return self.a + self.b
629+
630+
self.assertEqual(Sb1().do().shape(), unsigned(4))
631+
self.assertEqual(Sb2().do().shape(), unsigned(3))
632+
633+
def test_layout_redefined_wrong(self):
634+
class Sb(Struct):
635+
a: 1
636+
637+
with self.assertRaisesRegex(TypeError,
638+
r"^Aggregate class 'Sd' must either inherits or specify a layout, not both$"):
639+
class Sd(Sb):
640+
b: 1
641+
578642

579643
class UnionTestCase(FHDLTestCase):
580644
def test_construct(self):

0 commit comments

Comments
 (0)