Repository: amaranth-lang/amaranth
Pull request #780 (2023-05-13 01:16:53)

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)`.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/780?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
> Merging [#780](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/780?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (6ea8603) into [main](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/54d5c4c047f0d8dd010a599dc1fccbeafe026995?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (54d5c4c) will **increase** coverage by `0.01%`.
> The diff coverage is `94.87%`.

```diff
@@            Coverage Diff             @@
##             main     #780      +/-   ##
==========================================
+ Coverage   82.17%   82.18%   +0.01%     
==========================================
  Files          51       51              
  Lines        6910     6914       +4     
  Branches     1683     1680       -3     
==========================================
+ Hits         5678     5682       +4     
  Misses       1035     1035              
  Partials      197      197              
```


| [Impacted Files](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/780?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Coverage Δ | |
|---|---|---|
| [amaranth/hdl/ast.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/780?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL2FzdC5weQ==) | `89.47% <80.00%> (-0.11%)` | :arrow_down: |
| [amaranth/lib/data.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/780?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvbGliL2RhdGEucHk=) | `99.35% <100.00%> (-0.02%)` | :arrow_down: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/780/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)


Patch:
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index 7fe1d56..33feba7 100644
--- a/amaranth/hdl/ast.py
+++ b/amaranth/hdl/ast.py
@@ -1,4 +1,5 @@
 from abc import ABCMeta, abstractmethod
+import inspect
 import warnings
 import functools
 from collections import OrderedDict
@@ -45,6 +46,9 @@ class ShapeCastable:
         if not hasattr(cls, "as_shape"):
             raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
                             f"the `as_shape` method")
+        if not (hasattr(cls, "__call__") and inspect.isfunction(cls.__call__)):
+            raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
+                            f"the `__call__` method")
         if not hasattr(cls, "const"):
             raise TypeError(f"Class '{cls.__name__}' deriving from `ShapeCastable` must override "
                             f"the `const` method")
@@ -949,8 +953,16 @@ class Repl(Value):
         return "(repl {!r} {})".format(self.value, self.count)
 
 
+class _SignalMeta(ABCMeta):
+    def __call__(cls, shape=None, src_loc_at=0, **kwargs):
+        signal = super().__call__(shape, **kwargs, src_loc_at=src_loc_at + 1)
+        if isinstance(shape, ShapeCastable):
+            return shape(signal)
+        return signal
+
+
 # @final
-class Signal(Value, DUID):
+class Signal(Value, DUID, metaclass=_SignalMeta):
     """A varying integer value.
 
     Parameters
diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py
index 981ca82..51aeb8d 100644
--- a/amaranth/lib/data.py
+++ b/amaranth/lib/data.py
@@ -390,6 +390,13 @@ class UnionLayout(Layout):
         """
         return max((field.width for field in self._fields.values()), default=0)
 
+    def const(self, init):
+        if init is not None and len(init) > 1:
+            raise ValueError("Initializer for at most one field can be provided for "
+                             "a union layout (specified: {})"
+                             .format(", ".join(init.keys())))
+        return super().const(init)
+
     def __repr__(self):
         return f"UnionLayout({self.members!r})"
 
@@ -609,33 +616,21 @@ class View(ValueCastable):
     classes provided in this module are subclasses of :class:`View` that also provide a concise way
     to define a layout.
     """
-    def __init__(self, layout, target=None, *, name=None, reset=None, reset_less=None,
-            attrs=None, decoder=None, src_loc_at=0):
+    def __init__(self, layout, target):
         try:
             cast_layout = Layout.cast(layout)
         except TypeError as e:
             raise TypeError("View layout must be a layout, not {!r}"
                             .format(layout)) from e
-        if target is not None:
-            if (name is not None or reset is not None or reset_less is not None or
-                    attrs is not None or decoder is not None):
-                raise ValueError("View target cannot be provided at the same time as any of "
-                                 "the Signal constructor arguments (name, reset, reset_less, "
-                                 "attrs, decoder)")
-            try:
-                cast_target = Value.cast(target)
-            except TypeError as e:
-                raise TypeError("View target must be a value-castable object, not {!r}"
-                                .format(target)) from e
-            if len(cast_target) != cast_layout.size:
-                raise ValueError("View target is {} bit(s) wide, which is not compatible with "
-                                 "the {} bit(s) wide view layout"
-                                 .format(len(cast_target), cast_layout.size))
-        else:
-            if reset_less is None:
-                reset_less = False
-            cast_target = Signal(layout, name=name, reset=reset, reset_less=reset_less,
-                attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
+        try:
+            cast_target = Value.cast(target)
+        except TypeError as e:
+            raise TypeError("View target must be a value-castable object, not {!r}"
+                            .format(target)) from e
+        if len(cast_target) != cast_layout.size:
+            raise ValueError("View target is {} bit(s) wide, which is not compatible with "
+                             "the {} bit(s) wide view layout"
+                             .format(len(cast_target), cast_layout.size))
         self.__orig_layout = layout
         self.__layout = cast_layout
         self.__target = cast_target
@@ -752,8 +747,8 @@ class _AggregateMeta(ShapeCastable, type):
         elif all(not hasattr(base, "_AggregateMeta__layout") for base in bases):
             # This is a leaf class with its own layout. It is shape-castable and can
             # be instantiated. It can also be subclassed, and used to share layout and behavior.
-            layout = dict()
-            reset  = dict()
+            layout  = dict()
+            default = dict()
             for name in {**namespace["__annotations__"]}:
                 try:
                     Shape.cast(namespace["__annotations__"][name])
@@ -762,15 +757,15 @@ class _AggregateMeta(ShapeCastable, type):
                     continue
                 layout[name] = namespace["__annotations__"].pop(name)
                 if name in namespace:
-                    reset[name] = namespace.pop(name)
+                    default[name] = namespace.pop(name)
             cls = type.__new__(metacls, name, bases, namespace)
             if cls.__layout_cls is UnionLayout:
-                if len(reset) > 1:
+                if len(default) > 1:
                     raise ValueError("Reset value for at most one field can be provided for "
                                      "a union class (specified: {})"
-                                     .format(", ".join(reset.keys())))
-            cls.__layout = cls.__layout_cls(layout)
-            cls.__reset  = reset
+                                     .format(", ".join(default.keys())))
+            cls.__layout  = cls.__layout_cls(layout)
+            cls.__default = default
             return cls
         else:
             # This is a class that has a base class with a layout and annotations. Such a class
@@ -785,28 +780,24 @@ class _AggregateMeta(ShapeCastable, type):
                             .format(cls.__module__, cls.__qualname__))
         return cls.__layout
 
-    def const(cls, init):
-        return cls.as_shape().const(init)
-
+    def __call__(cls, target):
+        # This method exists to pass the override check done by ShapeCastable.
+        return super().__call__(cls, target)
 
-class _Aggregate(View, metaclass=_AggregateMeta):
-    def __init__(self, target=None, *, name=None, reset=None, reset_less=None,
-            attrs=None, decoder=None, src_loc_at=0):
-        if self.__class__._AggregateMeta__layout_cls is UnionLayout:
-            if reset is not None and len(reset) > 1:
-                raise ValueError("Reset value for at most one field can be provided for "
+    def const(cls, init):
+        if cls.__layout_cls is UnionLayout:
+            if init is not None and len(init) > 1:
+                raise ValueError("Initializer for at most one field can be provided for "
                                  "a union class (specified: {})"
-                                 .format(", ".join(reset.keys())))
-        if target is None and hasattr(self.__class__, "_AggregateMeta__reset"):
-            if reset is None:
-                reset = self.__class__._AggregateMeta__reset
-            elif self.__class__._AggregateMeta__layout_cls is not UnionLayout:
-                reset = {**self.__class__._AggregateMeta__reset, **reset}
-        super().__init__(self.__class__, target, name=name, reset=reset, reset_less=reset_less,
-            attrs=attrs, decoder=decoder, src_loc_at=src_loc_at + 1)
+                                 .format(", ".join(init.keys())))
+            return cls.as_shape().const(init or cls.__default)
+        else:
+            fields = cls.__default.copy()
+            fields.update(init or {})
+            return cls.as_shape().const(fields)
 
 
-class Struct(_Aggregate):
+class Struct(View, metaclass=_AggregateMeta):
     """Structures defined with annotations.
 
     The :class:`Struct` base class is a subclass of :class:`View` that provides a concise way
@@ -842,14 +833,14 @@ class Struct(_Aggregate):
 
         >>> IEEE754Single.as_shape()
         StructLayout({'fraction': 23, 'exponent': 8, 'sign': 1})
-        >>> Signal(IEEE754Single).width
+        >>> Signal(IEEE754Single).as_value().width
         32
 
     Instances of this class can be used where :ref:`values <lang-values>` are expected:
 
     .. doctest::
 
-        >>> flt = IEEE754Single()
+        >>> flt = Signal(IEEE754Single)
         >>> Signal(32).eq(flt)
         (eq (sig $signal) (sig flt))
 
@@ -866,11 +857,11 @@ class Struct(_Aggregate):
 
     .. doctest::
 
-        >>> hex(IEEE754Single().as_value().reset)
+        >>> hex(Signal(IEEE754Single).as_value().reset)
         '0x3f800000'
-        >>> hex(IEEE754Single(reset={'sign': 1}).as_value().reset)
+        >>> hex(Signal(IEEE754Single, reset={'sign': 1}).as_value().reset)
         '0xbf800000'
-        >>> hex(IEEE754Single(reset={'exponent': 0}).as_value().reset)
+        >>> hex(Signal(IEEE754Single, reset={'exponent': 0}).as_value().reset)
         '0x0'
 
     Classes inheriting from :class:`Struct` can be used as base classes. The only restrictions
@@ -903,15 +894,15 @@ class Struct(_Aggregate):
         Traceback (most recent call last):
           ...
         TypeError: Aggregate class 'HasChecksum' does not have a defined shape
-        >>> bare = BareHeader(); bare.checksum()
+        >>> bare = Signal(BareHeader); bare.checksum()
         (+ (+ (+ (const 1'd0) (slice (sig bare) 0:8)) (slice (sig bare) 8:16)) (slice (sig bare) 16:24))
-        >>> param = HeaderWithParam(); param.checksum()
+        >>> param = Signal(HeaderWithParam); param.checksum()
         (+ (+ (+ (+ (const 1'd0) (slice (sig param) 0:8)) (slice (sig param) 8:16)) (slice (sig param) 16:24)) (slice (sig param) 24:32))
     """
     _AggregateMeta__layout_cls = StructLayout
 
 
-class Union(_Aggregate):
+class Union(View, metaclass=_AggregateMeta):
     """Unions defined with annotations.
 
     The :class:`Union` base class is a subclass of :class:`View` that provides a concise way
@@ -931,9 +922,9 @@ class Union(_Aggregate):
 
     .. doctest::
 
-        >>> VarInt().as_value().reset
+        >>> Signal(VarInt).as_value().reset
         256
-        >>> VarInt(reset={'int8': 10}).as_value().reset
+        >>> Signal(VarInt, reset={'int8': 10}).as_value().reset
         10
     """
     _AggregateMeta__layout_cls = UnionLayout
diff --git a/docs/stdlib/data.rst b/docs/stdlib/data.rst
index 1714a3d..e063550 100644
--- a/docs/stdlib/data.rst
+++ b/docs/stdlib/data.rst
@@ -56,7 +56,7 @@ While this implementation works, it is repetitive, error-prone, hard to read, an
         "blue":  5
     })
 
-    i_color = data.View(rgb565_layout)
+    i_color = Signal(rgb565_layout)
     o_gray  = Signal(8)
 
     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
         "valid":  4
     })
 
-    i_stream = data.View(input_layout)
+    i_stream = Signal(input_layout)
     r_accum  = Signal(32)
 
     m.d.sync += r_accum.eq(
@@ -120,7 +120,7 @@ In case the data has related operations or transformations, :class:`View` can be
 
 .. testcode::
 
-    class RGBPixelLayout(data.StructLayout):
+    class RGBLayout(data.StructLayout):
         def __init__(self, r_bits, g_bits, b_bits):
             super().__init__({
                 "red":   unsigned(r_bits),
@@ -129,20 +129,20 @@ In case the data has related operations or transformations, :class:`View` can be
             })
 
         def __call__(self, value):
-            return RGBPixelView(self, value)
+            return RGBView(self, value)
 
-    class RGBPixelView(data.View):
+    class RGBView(data.View):
         def brightness(self):
             return (self.red + self.green + self.blue)[-8:]
 
-Here, the ``RGBLayout`` class itself is :ref:`shape-castable <lang-shapecasting>` and can be used anywhere a shape is accepted:
+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``:
 
 .. doctest::
 
-   >>> pixel = Signal(RGBPixelLayout(5, 6, 5))
-   >>> len(pixel)
+   >>> pixel = Signal(RGBLayout(5, 6, 5))
+   >>> len(pixel.as_value())
    16
-   >>> RGBPixelView(RGBPixelLayout(5, 6, 5), pixel).red
+   >>> pixel.red
    (slice (sig pixel) 0:5)
 
 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:
 
 .. testcode::
 
-    cmd = Command()
+    cmd = Signal(Command)
 
     m.d.comb += [
         cmd.valid.eq(1),


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 88209bb..bfec166 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -145,8 +145,11 @@ class MockShapeCastable(ShapeCastable):
     def as_shape(self):
         return self.dest
 
-    def const(self, obj):
-        return Const(obj, self.dest)
+    def __call__(self, value):
+        return value
+
+    def const(self, init):
+        return Const(init, self.dest)
 
 
 class ShapeCastableTestCase(FHDLTestCase):
@@ -1004,6 +1007,9 @@ class SignalTestCase(FHDLTestCase):
             def as_shape(self):
                 return unsigned(8)
 
+            def __call__(self, value):
+                return value
+
             def const(self, init):
                 return int(init, 16)
 
diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py
index af54527..24b4c3c 100644
--- a/tests/test_lib_data.py
+++ b/tests/test_lib_data.py
@@ -16,6 +16,9 @@ class MockShapeCastable(ShapeCastable):
     def as_shape(self):
         return self.shape
 
+    def __call__(self, value):
+        return value
+
     def const(self, init):
         return Const(init, self.shape)
 
@@ -172,6 +175,12 @@ class UnionLayoutTestCase(TestCase):
                 r"^Union layout member shape must be a shape-castable object, not 1\.0$"):
             UnionLayout({"a": 1.0})
 
+    def test_const_two_members_wrong(self):
+        with self.assertRaisesRegex(ValueError,
+                r"^Initializer for at most one field can be provided for a union layout "
+                r"\(specified: a, b\)$"):
+            UnionLayout({"a": 1, "b": 2}).const(dict(a=1, b=2))
+
 
 class ArrayLayoutTestCase(TestCase):
     def test_construct(self):
@@ -382,13 +391,13 @@ class LayoutTestCase(FHDLTestCase):
         self.assertRepr(sl.const(None), "(const 3'd0)")
         self.assertRepr(sl.const({"a": 0b1, "b": 0b10}), "(const 3'd5)")
 
-        ul = UnionLayout({
-            "a": unsigned(1),
-            "b": unsigned(2)
+        fl = FlexibleLayout(2, {
+            "a": Field(unsigned(1), 0),
+            "b": Field(unsigned(2), 0)
         })
-        self.assertRepr(ul.const({"a": 0b11}), "(const 2'd1)")
-        self.assertRepr(ul.const({"b": 0b10}), "(const 2'd2)")
-        self.assertRepr(ul.const({"a": 0b1, "b": 0b10}), "(const 2'd2)")
+        self.assertRepr(fl.const({"a": 0b11}), "(const 2'd1)")
+        self.assertRepr(fl.const({"b": 0b10}), "(const 2'd2)")
+        self.assertRepr(fl.const({"a": 0b1, "b": 0b10}), "(const 2'd2)")
 
     def test_const_wrong(self):
         sl = StructLayout({"f": unsigned(1)})
@@ -402,6 +411,9 @@ class LayoutTestCase(FHDLTestCase):
             def as_shape(self):
                 return unsigned(8)
 
+            def __call__(self, value):
+                return value
+
             def const(self, init):
                 return int(init, 16)
 
@@ -418,8 +430,8 @@ class LayoutTestCase(FHDLTestCase):
             "a": unsigned(1),
             "b": unsigned(2)
         })
-        self.assertEqual(Signal(sl).reset, 0)
-        self.assertEqual(Signal(sl, reset={"a": 0b1, "b": 0b10}).reset, 5)
+        self.assertEqual(Signal(sl).as_value().reset, 0)
+        self.assertEqual(Signal(sl, reset={"a": 0b1, "b": 0b10}).as_value().reset, 5)
 
 
 class ViewTestCase(FHDLTestCase):
@@ -431,41 +443,24 @@ class ViewTestCase(FHDLTestCase):
         self.assertRepr(v["b"], "(slice (sig s) 1:3)")
 
     def test_construct_signal(self):
-        v = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}))
+        v = Signal(StructLayout({"a": unsigned(1), "b": unsigned(2)}))
         cv = Value.cast(v)
         self.assertIsInstance(cv, Signal)
         self.assertEqual(cv.shape(), unsigned(3))
         self.assertEqual(cv.name, "v")
 
-    def test_construct_signal_name(self):
-        v = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}), name="named")
-        self.assertEqual(Value.cast(v).name, "named")
-
     def test_construct_signal_reset(self):
-        v1 = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}),
-                  reset={"a": 0b1, "b": 0b10})
+        v1 = Signal(StructLayout({"a": unsigned(1), "b": unsigned(2)}),
+                   reset={"a": 0b1, "b": 0b10})
         self.assertEqual(Value.cast(v1).reset, 0b101)
-        v2 = View(StructLayout({"a": unsigned(1),
+        v2 = Signal(StructLayout({"a": unsigned(1),
                                 "b": StructLayout({"x": unsigned(1), "y": unsigned(1)})}),
-                  reset={"a": 0b1, "b": {"x": 0b0, "y": 0b1}})
+                   reset={"a": 0b1, "b": {"x": 0b0, "y": 0b1}})
         self.assertEqual(Value.cast(v2).reset, 0b101)
-        v3 = View(ArrayLayout(unsigned(2), 2),
-                  reset=[0b01, 0b10])
+        v3 = Signal(ArrayLayout(unsigned(2), 2),
+                   reset=[0b01, 0b10])
         self.assertEqual(Value.cast(v3).reset, 0b1001)
 
-    def test_construct_signal_reset_less(self):
-        v = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}), reset_less=True)
-        self.assertEqual(Value.cast(v).reset_less, True)
-
-    def test_construct_signal_attrs(self):
-        v = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}), attrs={"debug": 1})
-        self.assertEqual(Value.cast(v).attrs, {"debug": 1})
-
-    def test_construct_signal_decoder(self):
-        decoder = lambda x: f"{x}"
-        v = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}), decoder=decoder)
-        self.assertEqual(Value.cast(v).decoder, decoder)
-
     def test_layout_wrong(self):
         with self.assertRaisesRegex(TypeError,
                 r"^View layout must be a layout, not <.+?>$"):
@@ -482,19 +477,8 @@ class ViewTestCase(FHDLTestCase):
                 r"wide view layout$"):
             View(StructLayout({"a": unsigned(1)}), Signal(2))
 
-    def test_signal_reset_wrong(self):
-        with self.assertRaisesRegex(TypeError,
-                r"^Reset value must be a constant initializer of StructLayout\({}\)$"):
-            View(StructLayout({}), reset=0b1)
-
-    def test_target_signal_wrong(self):
-        with self.assertRaisesRegex(ValueError,
-                r"^View target cannot be provided at the same time as any of the Signal "
-                r"constructor arguments \(name, reset, reset_less, attrs, decoder\)$"):
-            View(StructLayout({}), Signal(), reset=0b1)
-
     def test_getitem(self):
-        v = View(UnionLayout({
+        v = Signal(UnionLayout({
             "a": unsigned(2),
             "s": StructLayout({
                 "b": unsigned(1),
@@ -536,7 +520,7 @@ class ViewTestCase(FHDLTestCase):
             def const(self, init):
                 return Const(init, 2)
 
-        v = View(StructLayout({
+        v = Signal(StructLayout({
             "f": Reverser()
         }))
         self.assertRepr(v.f, "(cat (slice (slice (sig v) 0:2) 1:2) "
@@ -553,7 +537,7 @@ class ViewTestCase(FHDLTestCase):
             def const(self, init):
                 return Const(init, 2)
 
-        v = View(StructLayout({
+        v = Signal(StructLayout({
             "f": WrongCastable()
         }))
         with self.assertRaisesRegex(TypeError,
@@ -564,16 +548,16 @@ class ViewTestCase(FHDLTestCase):
     def test_index_wrong_missing(self):
         with self.assertRaisesRegex(KeyError,
                 r"^'a'$"):
-            View(StructLayout({}))["a"]
+            Signal(StructLayout({}))["a"]
 
     def test_index_wrong_struct_dynamic(self):
         with self.assertRaisesRegex(TypeError,
                 r"^Only views with array layout, not StructLayout\(\{\}\), may be indexed "
                 r"with a value$"):
-            View(StructLayout({}))[Signal(1)]
+            Signal(StructLayout({}))[Signal(1)]
 
     def test_getattr(self):
-        v = View(UnionLayout({
+        v = Signal(UnionLayout({
             "a": unsigned(2),
             "s": StructLayout({
                 "b": unsigned(1),
@@ -595,7 +579,7 @@ class ViewTestCase(FHDLTestCase):
         self.assertEqual(v.q.shape(), signed(1))
 
     def test_getattr_reserved(self):
-        v = View(UnionLayout({
+        v = Signal(UnionLayout({
             "_a": unsigned(2)
         }))
         self.assertRepr(v["_a"], "(slice (sig v) 0:2)")
@@ -604,13 +588,13 @@ class ViewTestCase(FHDLTestCase):
         with self.assertRaisesRegex(AttributeError,
                 r"^View of \(sig \$signal\) does not have a field 'a'; "
                 r"did you mean one of: 'b', 'c'\?$"):
-            View(StructLayout({"b": unsigned(1), "c": signed(1)})).a
+            Signal(StructLayout({"b": unsigned(1), "c": signed(1)})).a
 
     def test_attr_wrong_reserved(self):
         with self.assertRaisesRegex(AttributeError,
                 r"^View of \(sig \$signal\) field '_c' has a reserved name "
                 r"and may only be accessed by indexing$"):
-            View(StructLayout({"_c": signed(1)}))._c
+            Signal(StructLayout({"_c": signed(1)}))._c
 
 
 class StructTestCase(FHDLTestCase):
@@ -625,7 +609,7 @@ class StructTestCase(FHDLTestCase):
             "b": signed(3)
         }))
 
-        v = S()
+        v = Signal(S)
         self.assertEqual(Layout.of(v), S)
         self.assertEqual(Value.cast(v).shape(), Shape.cast(S))
         self.assertEqual(Value.cast(v).name, "v")
@@ -645,7 +629,7 @@ class StructTestCase(FHDLTestCase):
 
         self.assertEqual(Shape.cast(S), unsigned(9))
 
-        v = S()
+        v = Signal(S)
         self.assertIs(Layout.of(v), S)
         self.assertIsInstance(v, S)
         self.assertIs(Layout.of(v.b), R)
@@ -657,17 +641,6 @@ class StructTestCase(FHDLTestCase):
         self.assertRepr(v.b.q.r, "(s (slice (slice (slice (sig v) 1:9) 4:8) 0:2))")
         self.assertRepr(v.b.q.s, "(s (slice (slice (slice (sig v) 1:9) 4:8) 2:4))")
 
-    def test_construct_signal_kwargs(self):
-        decoder = lambda x: f"{x}"
-        v = View(StructLayout({"a": unsigned(1), "b": unsigned(2)}),
-            name="named", reset={"b": 0b1}, reset_less=True, attrs={"debug": 1}, decoder=decoder)
-        s = Value.cast(v)
-        self.assertEqual(s.name, "named")
-        self.assertEqual(s.reset, 0b010)
-        self.assertEqual(s.reset_less, True)
-        self.assertEqual(s.attrs, {"debug": 1})
-        self.assertEqual(s.decoder, decoder)
-
     def test_construct_reset(self):
         class S(Struct):
             p: 4
@@ -676,11 +649,11 @@ class StructTestCase(FHDLTestCase):
         with self.assertRaises(AttributeError):
             S.q
 
-        v1 = S()
+        v1 = Signal(S)
         self.assertEqual(v1.as_value().reset, 0b010000)
-        v2 = S(reset=dict(p=0b0011))
+        v2 = Signal(S, reset=dict(p=0b0011))
         self.assertEqual(v2.as_value().reset, 0b010011)
-        v3 = S(reset=dict(p=0b0011, q=0b00))
+        v3 = Signal(S, reset=dict(p=0b0011, q=0b00))
         self.assertEqual(v3.as_value().reset, 0b000011)
 
     def test_shape_undefined_wrong(self):
@@ -704,8 +677,8 @@ class StructTestCase(FHDLTestCase):
             a: 2
             b: 2
 
-        self.assertEqual(Sb1().add().shape(), unsigned(2))
-        self.assertEqual(Sb2().add().shape(), unsigned(3))
+        self.assertEqual(Signal(Sb1).add().shape(), unsigned(2))
+        self.assertEqual(Signal(Sb2).add().shape(), unsigned(3))
 
     def test_base_class_2(self):
         class Sb(Struct):
@@ -720,8 +693,8 @@ class StructTestCase(FHDLTestCase):
             def do(self):
                 return self.a + self.b
 
-        self.assertEqual(Sb1().do().shape(), unsigned(4))
-        self.assertEqual(Sb2().do().shape(), unsigned(3))
+        self.assertEqual(Signal(Sb1).do().shape(), unsigned(4))
+        self.assertEqual(Signal(Sb2).do().shape(), unsigned(3))
 
     def test_layout_redefined_wrong(self):
         class Sb(Struct):
@@ -738,7 +711,7 @@ class StructTestCase(FHDLTestCase):
             b: int
             c: str = "x"
 
-        self.assertEqual(Layout.of(S()), StructLayout({"a": unsigned(1)}))
+        self.assertEqual(Layout.of(Signal(S)), StructLayout({"a": unsigned(1)}))
         self.assertEqual(S.__annotations__, {"b": int, "c": str})
         self.assertEqual(S.c, "x")
 
@@ -755,23 +728,12 @@ class UnionTestCase(FHDLTestCase):
             "b": signed(3)
         }))
 
-        v = U()
+        v = Signal(U)
         self.assertEqual(Layout.of(v), U)
         self.assertEqual(Value.cast(v).shape(), Shape.cast(U))
         self.assertRepr(v.a, "(slice (sig v) 0:1)")
         self.assertRepr(v.b, "(s (slice (sig v) 0:3))")
 
-    def test_construct_signal_kwargs(self):
-        decoder = lambda x: f"{x}"
-        v = View(UnionLayout({"a": unsigned(1), "b": unsigned(2)}),
-            name="named", reset={"b": 0b1}, reset_less=True, attrs={"debug": 1}, decoder=decoder)
-        s = Value.cast(v)
-        self.assertEqual(s.name, "named")
-        self.assertEqual(s.reset, 0b01)
-        self.assertEqual(s.reset_less, True)
-        self.assertEqual(s.attrs, {"debug": 1})
-        self.assertEqual(s.decoder, decoder)
-
     def test_define_reset_two_wrong(self):
         with self.assertRaisesRegex(ValueError,
                 r"^Reset value for at most one field can be provided for a union class "
@@ -785,18 +747,20 @@ class UnionTestCase(FHDLTestCase):
             a: unsigned(1)
             b: unsigned(2)
 
-        with self.assertRaisesRegex(ValueError,
-                r"^Reset value for at most one field can be provided for a union class "
-                r"\(specified: a, b\)$"):
-            U(reset=dict(a=1, b=2))
+        with self.assertRaisesRegex(TypeError,
+                r"^Reset value must be a constant initializer of <class '.+?\.U'>$") as cm:
+            Signal(U, reset=dict(a=1, b=2))
+            self.assertRegex(cm.exception.__cause__.message,
+                             r"^Initializer for at most one field can be provided for a union "
+                             r"class \(specified: a, b\)$")
 
     def test_construct_reset_override(self):
         class U(Union):
             a: unsigned(1) = 1
             b: unsigned(2)
 
-        self.assertEqual(U().as_value().reset, 0b01)
-        self.assertEqual(U(reset=dict(b=0b10)).as_value().reset, 0b10)
+        self.assertEqual(Signal(U).as_value().reset, 0b01)
+        self.assertEqual(Signal(U, reset=dict(b=0b10)).as_value().reset, 0b10)
 
 
 # Examples from https://github.com/amaranth-lang/amaranth/issues/693
@@ -817,7 +781,7 @@ class RFCExamplesTestCase(TestCase):
 
         self.assertEqual(Float32.as_shape().size, 32)
 
-        flt_a = Float32()
+        flt_a = Float32(Signal(32))
         flt_b = Float32(Const(0b00111110001000000000000000000000, 32))
 
         m1 = Module()
@@ -835,7 +799,7 @@ class RFCExamplesTestCase(TestCase):
             float: Float32
             int: signed(32)
 
-        f_or_i = FloatOrInt32()
+        f_or_i = Signal(FloatOrInt32)
         is_gt_1 = Signal()
         m2 = Module()
         m2.d.comb += [
@@ -857,10 +821,9 @@ class RFCExamplesTestCase(TestCase):
             "b": Float32
         })
 
-        adder_op_storage = Signal(adder_op_layout)
-        self.assertEqual(len(adder_op_storage), 65)
+        adder_op = Signal(adder_op_layout)
+        self.assertEqual(len(adder_op.as_value()), 65)
 
-        adder_op = View(adder_op_layout, adder_op_storage)
         m3 = Module()
         m3.d.comb += [
             adder_op.eq(Op.SUB),
@@ -886,15 +849,10 @@ class RFCExamplesTestCase(TestCase):
         })
         self.assertEqual(layout1.size, 3)
 
-        sig1 = Signal(layout1)
-        self.assertEqual(sig1.shape(), unsigned(3))
-
-        view1 = View(layout1, sig1)
-        self.assertIs(Value.cast(view1), sig1)
-
-        view2 = View(layout1)
-        self.assertIsInstance(Value.cast(view2), Signal)
-        self.assertEqual(Value.cast(view2).shape(), unsigned(3))
+        view1 = Signal(layout1)
+        self.assertIsInstance(view1, View)
+        self.assertEqual(Layout.of(view1), layout1)
+        self.assertEqual(view1.as_value().shape(), unsigned(3))
 
         m1 = Module()
         m1.d.comb += [
@@ -916,23 +874,20 @@ class RFCExamplesTestCase(TestCase):
 
         self.assertEqual(Shape.cast(SomeVariant), unsigned(3))
 
-        view3 = SomeVariant()
-        self.assertIsInstance(Value.cast(view3), Signal)
-        self.assertEqual(Value.cast(view3).shape(), unsigned(3))
+        view2 = Signal(SomeVariant)
+        self.assertIsInstance(Value.cast(view2), Signal)
+        self.assertEqual(Value.cast(view2).shape(), unsigned(3))
 
         m2 = Module()
         m2.submodules += m1
         m2.d.comb += [
-            view3.kind.eq(Kind.ONE_SIGNED),
-            view3.value.eq(view1.value)
+            view2.kind.eq(Kind.ONE_SIGNED),
+            view2.value.eq(view1.value)
         ]
 
         @self.simulate(m2)
         def check_m2():
-            self.assertEqual((yield view3.as_value()), 0b010)
-
-        sig2 = Signal(SomeVariant)
-        self.assertEqual(sig2.shape(), unsigned(3))
+            self.assertEqual((yield view2.as_value()), 0b010)
 
         layout2 = StructLayout({
             "ready": unsigned(1),
@@ -942,18 +897,4 @@ class RFCExamplesTestCase(TestCase):
 
         self.assertEqual(layout1, Layout.cast(SomeVariant))
 
-        self.assertIs(SomeVariant, Layout.of(view3))
-
-    def test_rfc_example_3(self):
-        class Stream8b10b(View):
-            data: Signal
-            ctrl: Signal
-
-            def __init__(self, value=None, *, width: int):
-                super().__init__(StructLayout({
-                    "data": unsigned(8 * width),
-                    "ctrl": unsigned(width)
-                }), value)
-
-        self.assertEqual(len(Stream8b10b(width=1).data), 8)
-        self.assertEqual(len(Stream8b10b(width=4).data), 32)
+        self.assertIs(SomeVariant, Layout.of(view2))