diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 5ff9a6443..a0bbdd0ad 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -62,11 +62,11 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        project:
-          - amaranth-lang/amaranth-boards
-          - amaranth-lang/amaranth-stdio
-          - amaranth-lang/amaranth-soc
-    name: 'smoke (${{ matrix.project }})'
+        project: # test the last commit before dropping py3.8
+          - { name: amaranth-lang/amaranth-boards, ref: 19b97324ecf9111c5d16377af79f82aad761c476 }
+          - { name: amaranth-lang/amaranth-stdio, ref: 2da45b8e75421879d1096495a4fb438de705f567 }
+          - { name: amaranth-lang/amaranth-soc, ref: 746709e1e992bccf6e2362450243cafd00d72a14 }
+    name: 'smoke (${{ matrix.project.name }})'
     steps:
     - name: Check out Amaranth source code
       uses: actions/checkout@v4
@@ -76,7 +76,8 @@ jobs:
     - name: Check out source code
       uses: actions/checkout@v4
       with:
-        repository: ${{ matrix.project }}
+        repository: ${{ matrix.project.name }}
+        ref: ${{ matrix.project.ref }}
         path: project
         fetch-depth: 0
     - name: Set up PDM
diff --git a/amaranth/_unused.py b/amaranth/_unused.py
index 6e053435a..4f2507bc1 100644
--- a/amaranth/_unused.py
+++ b/amaranth/_unused.py
@@ -32,7 +32,7 @@ def __del__(self):
             return
         if hasattr(self, "_MustUse__used") and not self._MustUse__used:
             if get_linter_option(self._MustUse__context["filename"],
-                                 self._MustUse__warning.__name__, bool, True):
+                                 self._MustUse__warning.__qualname__, bool, True):
                 warnings.warn_explicit(
                     f"{self!r} created but never used", self._MustUse__warning,
                     **self._MustUse__context)
diff --git a/amaranth/_utils.py b/amaranth/_utils.py
index 938461527..10b125b16 100644
--- a/amaranth/_utils.py
+++ b/amaranth/_utils.py
@@ -44,7 +44,7 @@ def union(i, start=None):
 def final(cls):
     def init_subclass():
         raise TypeError("Subclassing {}.{} is not supported"
-                        .format(cls.__module__, cls.__name__))
+                        .format(cls.__module__, cls.__qualname__))
     cls.__init_subclass__ = init_subclass
     return cls
 
diff --git a/amaranth/back/cxxrtl.py b/amaranth/back/cxxrtl.py
index 0fa657b0a..725bda48a 100644
--- a/amaranth/back/cxxrtl.py
+++ b/amaranth/back/cxxrtl.py
@@ -23,8 +23,8 @@ def _convert_rtlil_text(rtlil_text, black_boxes, *, src_loc_at=0):
     script = []
     if black_boxes is not None:
         for box_name, box_source in black_boxes.items():
-            script.append(f"read_ilang <<rtlil\n{box_source}\nrtlil")
-    script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil")
+            script.append(f"read_rtlil <<rtlil\n{box_source}\nrtlil")
+    script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil")
     script.append("write_cxxrtl")
 
     return yosys.run(["-q", "-"], "\n".join(script), src_loc_at=1 + src_loc_at)
diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py
index cecb11f0f..28f428c1a 100644
--- a/amaranth/back/rtlil.py
+++ b/amaranth/back/rtlil.py
@@ -1132,7 +1132,7 @@ def emit_print(self, cell_idx, cell):
         if cell.format is not None:
             for chunk in cell.format.chunks:
                 if isinstance(chunk, str):
-                    format.append(chunk)
+                    format.append(chunk.replace("{", "{{").replace("}", "}}"))
                 else:
                     spec = _ast.Format._parse_format_spec(chunk.format_desc, _ast.Shape(len(chunk.value), chunk.signed))
                     type = spec["type"]
diff --git a/amaranth/back/verilog.py b/amaranth/back/verilog.py
index 6aba92982..5734df62d 100644
--- a/amaranth/back/verilog.py
+++ b/amaranth/back/verilog.py
@@ -12,7 +12,7 @@ def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog
     yosys = find_yosys(lambda ver: ver >= (0, 40))
 
     script = []
-    script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil")
+    script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil")
     script.append("proc -nomux -norom")
     script.append("memory_collect")
 
diff --git a/amaranth/build/plat.py b/amaranth/build/plat.py
index 432ae6770..02dbbaa3b 100644
--- a/amaranth/build/plat.py
+++ b/amaranth/build/plat.py
@@ -40,7 +40,7 @@ def __init__(self):
     def default_clk_constraint(self):
         if self.default_clk is None:
             raise AttributeError("Platform '{}' does not define a default clock"
-                                 .format(type(self).__name__))
+                                 .format(type(self).__qualname__))
         return self.lookup(self.default_clk).clock
 
     @property
@@ -48,7 +48,7 @@ def default_clk_frequency(self):
         constraint = self.default_clk_constraint
         if constraint is None:
             raise AttributeError("Platform '{}' does not constrain its default clock"
-                                 .format(type(self).__name__))
+                                 .format(type(self).__qualname__))
         return constraint.frequency
 
     def add_file(self, filename, content):
@@ -120,16 +120,22 @@ def create_missing_domain(self, name):
         # Many device families provide advanced primitives for tackling reset. If these exist,
         # they should be used instead.
         if name == "sync" and self.default_clk is not None:
-            clk_i = self.request(self.default_clk).i
+            m = Module()
+
+            clk_io = self.request(self.default_clk, dir="-")
+            m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
+
             if self.default_rst is not None:
-                rst_i = self.request(self.default_rst).i
+                rst_io = self.request(self.default_rst, dir="-")
+                m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
+                rst_i = rst_buf.i
             else:
                 rst_i = Const(0)
 
-            m = Module()
             m.domains += ClockDomain("sync")
-            m.d.comb += ClockSignal("sync").eq(clk_i)
+            m.d.comb += ClockSignal("sync").eq(clk_buf.i)
             m.submodules.reset_sync = ResetSynchronizer(rst_i, domain="sync")
+
             return m
 
     def prepare(self, elaboratable, name="top", **kwargs):
@@ -173,7 +179,7 @@ def toolchain_program(self, products, name, **kwargs):
         Extract bitstream for fragment ``name`` from ``products`` and download it to a target.
         """
         raise NotImplementedError("Platform '{}' does not support programming"
-                                  .format(type(self).__name__))
+                                  .format(type(self).__qualname__))
 
 
 class TemplatedPlatform(Platform):
@@ -198,14 +204,14 @@ class TemplatedPlatform(Platform):
         """,
     }
 
-    def iter_clock_constraints(self):
-        for net_signal, port_signal, frequency in super().iter_clock_constraints():
+    def iter_signal_clock_constraints(self):
+        for signal, frequency in super().iter_signal_clock_constraints():
             # Skip any clock constraints placed on signals that are never used in the design.
             # Otherwise, it will cause a crash in the vendor platform if it supports clock
             # constraints on non-port nets.
-            if net_signal not in self._name_map:
+            if signal not in self._name_map:
                 continue
-            yield net_signal, port_signal, frequency
+            yield signal, frequency
 
     def toolchain_prepare(self, fragment, name, *, emit_src=True, **kwargs):
         # Restrict the name of the design to a strict alphanumeric character set. Platforms will
@@ -244,7 +250,7 @@ def _extract_override(var, *, expected_type):
                 if issubclass(expected_type, str) and not isinstance(kwarg, str) and isinstance(kwarg, Iterable):
                     kwarg = " ".join(kwarg)
                 if not isinstance(kwarg, expected_type) and not expected_type is None:
-                    raise TypeError(f"Override '{var}' must be a {expected_type.__name__}, not {kwarg!r}")
+                    raise TypeError(f"Override '{var}' must be a {expected_type.__qualname__}, not {kwarg!r}")
                 return kwarg
             else:
                 return jinja2.Undefined(name=var)
@@ -327,8 +333,11 @@ def options(opts):
             else:
                 return " ".join(opts)
 
-        def hierarchy(signal, separator):
-            return separator.join(self._name_map[signal][1:])
+        def hierarchy(net, separator):
+            if isinstance(net, IOPort):
+                return net.name
+            else:
+                return separator.join(self._name_map[net][1:])
 
         def ascii_escape(string):
             def escape_one(match):
diff --git a/amaranth/build/res.py b/amaranth/build/res.py
index 0744ef002..effe44e14 100644
--- a/amaranth/build/res.py
+++ b/amaranth/build/res.py
@@ -109,8 +109,9 @@ def __init__(self, resources, connectors):
 
         # List of (pin, port, buffer) pairs for non-dir="-" requests.
         self._pins      = []
-        # Constraint list
+        # Constraint lists
         self._clocks    = SignalDict()
+        self._io_clocks = {}
 
         self.add_resources(resources)
         self.add_connectors(connectors)
@@ -151,7 +152,8 @@ def request(self, name, number=0, *, dir=None, xdr=None):
 
         def merge_options(subsignal, dir, xdr):
             if isinstance(subsignal.ios[0], Subsignal):
-                if dir is None:
+                orig_dir = dir
+                if dir is None or dir == "-":
                     dir = dict()
                 if xdr is None:
                     xdr = dict()
@@ -164,7 +166,7 @@ def merge_options(subsignal, dir, xdr):
                                     "has subsignals"
                                     .format(xdr, subsignal))
                 for sub in subsignal.ios:
-                    sub_dir = dir.get(sub.name, None)
+                    sub_dir = "-" if orig_dir == "-" else dir.get(sub.name, None)
                     sub_xdr = xdr.get(sub.name, None)
                     dir[sub.name], xdr[sub.name] = merge_options(sub, sub_dir, sub_xdr)
             else:
@@ -219,6 +221,8 @@ def resolve(resource, dir, xdr, path, attrs):
                         for name in phys_names
                     ])
                     port = io.SingleEndedPort(iop, invert=phys.invert, direction=direction)
+                    if resource.clock is not None:
+                        self.add_clock_constraint(iop, resource.clock.frequency)
                 if isinstance(phys, DiffPairs):
                     phys_names_p = phys.p.map_names(self._conn_pins, resource)
                     phys_names_n = phys.n.map_names(self._conn_pins, resource)
@@ -232,7 +236,8 @@ def resolve(resource, dir, xdr, path, attrs):
                         for name in phys_names_n
                     ])
                     port = io.DifferentialPort(p, n, invert=phys.invert, direction=direction)
-
+                    if resource.clock is not None:
+                        self.add_clock_constraint(p, resource.clock.frequency)
                 for phys_name in phys_names:
                     if phys_name in self._phys_reqd:
                         raise ResourceError("Resource component {} uses physical pin {}, but it "
@@ -253,8 +258,6 @@ def resolve(resource, dir, xdr, path, attrs):
                     buffer = PinBuffer(pin, port)
                     self._pins.append((pin, port, buffer))
 
-                    if resource.clock is not None:
-                        self.add_clock_constraint(pin.i, resource.clock.frequency)
                     return pin
 
             else:
@@ -275,38 +278,28 @@ def add_clock_constraint(self, clock, frequency):
             raise TypeError(f"A clock constraint can only be applied to a Signal, but a "
                             f"ClockSignal is provided; assign the ClockSignal to an "
                             f"intermediate signal and constrain the latter instead.")
-        elif not isinstance(clock, Signal):
-            raise TypeError(f"Object {clock!r} is not a Signal")
+        elif not isinstance(clock, (Signal, IOPort)):
+            raise TypeError(f"Object {clock!r} is not a Signal or IOPort")
         if not isinstance(frequency, (int, float)):
             raise TypeError(f"Frequency must be a number, not {frequency!r}")
 
-        if clock in self._clocks:
+        if isinstance(clock, IOPort):
+            clocks = self._io_clocks
+        else:
+            clocks = self._clocks
+
+        frequency = float(frequency)
+        if clock in clocks and clocks[clock] != frequency:
             raise ValueError("Cannot add clock constraint on {!r}, which is already constrained "
                              "to {} Hz"
-                             .format(clock, self._clocks[clock]))
+                             .format(clock, clocks[clock]))
         else:
-            self._clocks[clock] = float(frequency)
-
-    def iter_clock_constraints(self):
-        # Back-propagate constraints through the input buffer. For clock constraints on pins
-        # (the majority of cases), toolchains work better if the constraint is defined on the pin
-        # and not on the buffered internal net; and if the toolchain is advanced enough that
-        # it considers clock phase and delay of the input buffer, it is *necessary* to define
-        # the constraint on the pin to match the designer's expectation of phase being referenced
-        # to the pin.
-        #
-        # Constraints on nets with no corresponding input pin (e.g. PLL or SERDES outputs) are not
-        # affected.
-        pin_i_to_port = SignalDict()
-        for pin, port, _fragment in self._pins:
-            if hasattr(pin, "i"):
-                if isinstance(port, io.SingleEndedPort):
-                    pin_i_to_port[pin.i] = port.io
-                elif isinstance(port, io.DifferentialPort):
-                    pin_i_to_port[pin.i] = port.p
-                else:
-                    assert False
+            clocks[clock] = frequency
+
+    def iter_signal_clock_constraints(self):
+        for signal, frequency in self._clocks.items():
+            yield signal, frequency
 
-        for net_signal, frequency in self._clocks.items():
-            port_signal = pin_i_to_port.get(net_signal)
-            yield net_signal, port_signal, frequency
+    def iter_port_clock_constraints(self):
+        for port, frequency in self._io_clocks.items():
+            yield port, frequency
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 8eea92782..e523a95c4 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -244,16 +244,16 @@ def __init__(self, *args, **kwargs):
 
     def __init_subclass__(cls, **kwargs):
         if cls.as_shape is ShapeCastable.as_shape:
-            raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
+            raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' must override "
                             f"the 'as_shape' method")
         if cls.const is ShapeCastable.const:
-            raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
+            raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' must override "
                             f"the 'const' method")
         if cls.__call__ is ShapeCastable.__call__:
-            raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
+            raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' must override "
                             f"the '__call__' method")
         if cls.from_bits is ShapeCastable.from_bits:
-            warnings.warn(f"Class '{cls.__name__}' deriving from 'ShapeCastable' does not override "
+            warnings.warn(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' does not override "
                           f"the 'from_bits' method, which will be required in Amaranth 0.6",
                           DeprecationWarning, stacklevel=2)
 
@@ -1404,10 +1404,10 @@ def __init__(self, *args, **kwargs):
 
     def __init_subclass__(cls, **kwargs):
         if cls.as_value is ValueCastable.as_value:
-            raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override "
+            raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ValueCastable' must override "
                             "the 'as_value' method")
         if cls.shape is ValueCastable.shape:
-            raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override "
+            raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ValueCastable' must override "
                             "the 'shape' method")
 
     # The signatures and definitions of these methods are weird because they are present here for
@@ -1598,6 +1598,10 @@ def cast(obj):
 
     def __init__(self, value, shape=None, *, src_loc_at=0):
         # We deliberately do not call Value.__init__ here.
+        if isinstance(value, Enum):
+            if shape is None:
+                shape = Shape.cast(type(value))
+            value = value.value
         value = int(operator.index(value))
         if shape is None:
             shape = Shape(bits_for(value), signed=value < 0)
@@ -1972,6 +1976,57 @@ def __call__(cls, shape=None, src_loc_at=0, **kwargs):
         return signal
 
 
+# also used for MemoryData.Init
+def _get_init_value(init, shape, what="signal"):
+    orig_init = init
+    orig_shape = shape
+    shape = Shape.cast(shape)
+    if isinstance(orig_shape, ShapeCastable):
+        try:
+            init = Const.cast(orig_shape.const(init))
+        except Exception:
+            raise TypeError(f"Initial value must be a constant initializer of {orig_shape!r}")
+        if init.shape() != Shape.cast(shape):
+            raise ValueError(f"Constant returned by {orig_shape!r}.const() must have the shape "
+                             f"that it casts to, {shape!r}, and not {init.shape()!r}")
+        return init.value
+    else:
+        if init is None:
+            init = 0
+        try:
+            init = Const.cast(init)
+        except TypeError:
+            raise TypeError("Initial value must be a constant-castable expression, not {!r}"
+                            .format(orig_init))
+        # Avoid false positives for all-zeroes and all-ones
+        if orig_init is not None and not (isinstance(orig_init, int) and orig_init in (0, -1)):
+            if init.shape().signed and not shape.signed:
+                warnings.warn(
+                    message=f"Initial value {orig_init!r} is signed, "
+                            f"but the {what} shape is {shape!r}",
+                    category=SyntaxWarning,
+                    stacklevel=2)
+            elif (init.shape().width > shape.width or
+                  init.shape().width == shape.width and
+                    shape.signed and not init.shape().signed):
+                warnings.warn(
+                    message=f"Initial value {orig_init!r} will be truncated to "
+                            f"the {what} shape {shape!r}",
+                    category=SyntaxWarning,
+                    stacklevel=2)
+
+        if isinstance(orig_shape, range) and orig_init is not None and orig_init not in orig_shape:
+            if orig_init == orig_shape.stop:
+                raise SyntaxError(
+                    f"Initial value {orig_init!r} equals the non-inclusive end of the {what} "
+                    f"shape {orig_shape!r}; this is likely an off-by-one error")
+            else:
+                raise SyntaxError(
+                    f"Initial value {orig_init!r} is not within the {what} shape {orig_shape!r}")
+
+        return Const(init.value, shape).value
+
+
 @final
 class Signal(Value, DUID, metaclass=_SignalMeta):
     """A varying integer value.
@@ -2042,65 +2097,20 @@ def __init__(self, shape=None, *, name=None, init=None, reset=None, reset_less=F
                           DeprecationWarning, stacklevel=2)
             init = reset
 
-        orig_init = init
-        if isinstance(orig_shape, ShapeCastable):
-            try:
-                init = Const.cast(orig_shape.const(init))
-            except Exception:
-                raise TypeError("Initial value must be a constant initializer of {!r}"
-                                .format(orig_shape))
-            if init.shape() != Shape.cast(orig_shape):
-                raise ValueError("Constant returned by {!r}.const() must have the shape that "
-                                 "it casts to, {!r}, and not {!r}"
-                                 .format(orig_shape, Shape.cast(orig_shape),
-                                         init.shape()))
-        else:
-            if init is None:
-                init = 0
-            try:
-                init = Const.cast(init)
-            except TypeError:
-                raise TypeError("Initial value must be a constant-castable expression, not {!r}"
-                                .format(orig_init))
-        # Avoid false positives for all-zeroes and all-ones
-        if orig_init is not None and not (isinstance(orig_init, int) and orig_init in (0, -1)):
-            if init.shape().signed and not self._signed:
-                warnings.warn(
-                    message="Initial value {!r} is signed, but the signal shape is {!r}"
-                            .format(orig_init, shape),
-                    category=SyntaxWarning,
-                    stacklevel=2)
-            elif (init.shape().width > self._width or
-                  init.shape().width == self._width and
-                    self._signed and not init.shape().signed):
-                warnings.warn(
-                    message="Initial value {!r} will be truncated to the signal shape {!r}"
-                            .format(orig_init, shape),
-                    category=SyntaxWarning,
-                    stacklevel=2)
-        self._init = init.value
+        self._init = _get_init_value(init, unsigned(1) if orig_shape is None else orig_shape)
         self._reset_less = bool(reset_less)
 
-        if isinstance(orig_shape, range) and orig_init is not None and orig_init not in orig_shape:
-            if orig_init == orig_shape.stop:
-                raise SyntaxError(
-                    f"Initial value {orig_init!r} equals the non-inclusive end of the signal "
-                    f"shape {orig_shape!r}; this is likely an off-by-one error")
-            else:
-                raise SyntaxError(
-                    f"Initial value {orig_init!r} is not within the signal shape {orig_shape!r}")
-
         self._attrs = OrderedDict(() if attrs is None else attrs)
 
         if isinstance(orig_shape, ShapeCastable):
             self._format = orig_shape.format(orig_shape(self), "")
         elif isinstance(orig_shape, type) and issubclass(orig_shape, Enum):
-            self._format = Format.Enum(self, orig_shape, name=orig_shape.__name__)
+            self._format = Format.Enum(self, orig_shape, name=orig_shape.__qualname__)
         else:
             self._format = Format("{}", self)
 
         if isinstance(decoder, type) and issubclass(decoder, Enum):
-            self._format = Format.Enum(self, decoder, name=decoder.__name__)
+            self._format = Format.Enum(self, decoder, name=decoder.__qualname__)
 
         self._decoder = decoder
 
@@ -2552,7 +2562,7 @@ def __format__(self, format_desc):
 @final
 class Format(_FormatLike):
     def __init__(self, format, *args, **kwargs):
-        fmt = string.Formatter()
+        fmtter = string.Formatter()
         chunks = []
         used_args = set()
         auto_arg_index = 0
@@ -2573,29 +2583,29 @@ def get_field(field_name):
                                         "specification")
                 auto_arg_index = None
 
-            obj, arg_used = fmt.get_field(field_name, args, kwargs)
+            obj, arg_used = fmtter.get_field(field_name, args, kwargs)
             used_args.add(arg_used)
             return obj
 
         def subformat(sub_string):
             result = []
-            for literal, field_name, format_spec, conversion in fmt.parse(sub_string):
+            for literal, field_name, format_spec, conversion in fmtter.parse(sub_string):
                 result.append(literal)
                 if field_name is not None:
                     obj = get_field(field_name)
-                    obj = fmt.convert_field(obj, conversion)
+                    obj = fmtter.convert_field(obj, conversion)
                     format_spec = subformat(format_spec)
-                    result.append(fmt.format_field(obj, format_spec))
+                    result.append(fmtter.format_field(obj, format_spec))
             return "".join(result)
 
-        for literal, field_name, format_spec, conversion in fmt.parse(format):
+        for literal, field_name, format_spec, conversion in fmtter.parse(format):
             chunks.append(literal)
             if field_name is not None:
                 obj = get_field(field_name)
                 if conversion == "v":
                     obj = Value.cast(obj)
                 else:
-                    obj = fmt.convert_field(obj, conversion)
+                    obj = fmtter.convert_field(obj, conversion)
                 format_spec = subformat(format_spec)
                 if isinstance(obj, Value):
                     # Perform validation.
@@ -2617,7 +2627,7 @@ def subformat(sub_string):
                         raise ValueError(f"Format specifiers ({format_spec!r}) cannot be used for 'Format' objects")
                     chunks += obj._as_format()._chunks
                 else:
-                    chunks.append(fmt.format_field(obj, format_spec))
+                    chunks.append(fmtter.format_field(obj, format_spec))
 
         for i in range(len(args)):
             if i not in used_args:
@@ -3241,7 +3251,7 @@ def __len__(self):
 
     def __repr__(self):
         pairs = [f"({k!r}, {v!r})" for k, v in self.items()]
-        return "{}.{}([{}])".format(type(self).__module__, type(self).__name__,
+        return "{}.{}([{}])".format(type(self).__module__, type(self).__qualname__,
                                     ", ".join(pairs))
 
 
@@ -3273,7 +3283,7 @@ def __len__(self):
         return len(self._storage)
 
     def __repr__(self):
-        return "{}.{}({})".format(type(self).__module__, type(self).__name__,
+        return "{}.{}({})".format(type(self).__module__, type(self).__qualname__,
                                   ", ".join(repr(x) for x in self))
 
 
@@ -3303,7 +3313,7 @@ def __lt__(self, other):
         return self._intern < other._intern
 
     def __repr__(self):
-        return f"<{__name__}.SignalKey {self.signal!r}>"
+        return f"<{type(self).__qualname__} {self.signal!r}>"
 
 
 class SignalDict(_MappedKeyDict):
diff --git a/amaranth/hdl/_dsl.py b/amaranth/hdl/_dsl.py
index c66df2e21..9cf0b2b4b 100644
--- a/amaranth/hdl/_dsl.py
+++ b/amaranth/hdl/_dsl.py
@@ -19,82 +19,78 @@
 __all__ = ["SyntaxError", "SyntaxWarning", "Module"]
 
 
-class _Visitor:
-    def __init__(self):
-        self.driven_signals = SignalSet()
-
-    def visit_stmt(self, stmt):
-        if isinstance(stmt, _StatementList):
-            for s in stmt:
-                self.visit_stmt(s)
-        elif isinstance(stmt, Assign):
-            self.visit_lhs(stmt.lhs)
-            self.visit_rhs(stmt.rhs)
-        elif isinstance(stmt, Print):
+def _check_stmt(stmt):
+    if isinstance(stmt, _StatementList):
+        for s in stmt:
+            _check_stmt(s)
+    elif isinstance(stmt, Assign):
+        _check_lhs(stmt.lhs)
+        _check_rhs(stmt.rhs)
+    elif isinstance(stmt, Print):
+        for chunk in stmt.message._chunks:
+            if not isinstance(chunk, str):
+                obj, format_spec = chunk
+                _check_rhs(obj)
+    elif isinstance(stmt, Property):
+        _check_rhs(stmt.test)
+        if stmt.message is not None:
             for chunk in stmt.message._chunks:
                 if not isinstance(chunk, str):
                     obj, format_spec = chunk
-                    self.visit_rhs(obj)
-        elif isinstance(stmt, Property):
-            self.visit_rhs(stmt.test)
-            if stmt.message is not None:
-                for chunk in stmt.message._chunks:
-                    if not isinstance(chunk, str):
-                        obj, format_spec = chunk
-                        self.visit_rhs(obj)
-        elif isinstance(stmt, Switch):
-            self.visit_rhs(stmt.test)
-            for _patterns, stmts, _src_loc in stmt.cases:
-                self.visit_stmt(stmts)
-        elif isinstance(stmt, _LateBoundStatement):
-            pass
-        else:
-            assert False # :nocov:
-
-    def visit_lhs(self, value):
-        if isinstance(value, Operator) and value.operator in ("u", "s"):
-            self.visit_lhs(value.operands[0])
-        elif isinstance(value, (Signal, ClockSignal, ResetSignal)):
-            self.driven_signals.add(value)
-        elif isinstance(value, Slice):
-            self.visit_lhs(value.value)
-        elif isinstance(value, Part):
-            self.visit_lhs(value.value)
-            self.visit_rhs(value.offset)
-        elif isinstance(value, Concat):
-            for part in value.parts:
-                self.visit_lhs(part)
-        elif isinstance(value, SwitchValue):
-            self.visit_rhs(value.test)
-            for _patterns, elem in value.cases:
-                self.visit_lhs(elem)
-        elif isinstance(value, MemoryData._Row):
-            raise ValueError(f"Value {value!r} can only be used in simulator processes")
-        else:
-            raise ValueError(f"Value {value!r} cannot be assigned to")
-
-    def visit_rhs(self, value):
-        if isinstance(value, (Const, Signal, ClockSignal, ResetSignal, Initial, AnyValue)):
-            pass
-        elif isinstance(value, Operator):
-            for op in value.operands:
-                self.visit_rhs(op)
-        elif isinstance(value, Slice):
-            self.visit_rhs(value.value)
-        elif isinstance(value, Part):
-            self.visit_rhs(value.value)
-            self.visit_rhs(value.offset)
-        elif isinstance(value, Concat):
-            for part in value.parts:
-                self.visit_rhs(part)
-        elif isinstance(value, SwitchValue):
-            self.visit_rhs(value.test)
-            for _patterns, elem in value.cases:
-                self.visit_rhs(elem)
-        elif isinstance(value, MemoryData._Row):
-            raise ValueError(f"Value {value!r} can only be used in simulator processes")
-        else:
-            assert False # :nocov:
+                    _check_rhs(obj)
+    elif isinstance(stmt, Switch):
+        _check_rhs(stmt.test)
+        for _patterns, stmts, _src_loc in stmt.cases:
+            _check_stmt(stmts)
+    elif isinstance(stmt, _LateBoundStatement):
+        pass
+    else:
+        assert False # :nocov:
+
+def _check_lhs(value):
+    if isinstance(value, Operator) and value.operator in ("u", "s"):
+        _check_lhs(value.operands[0])
+    elif isinstance(value, (Signal, ClockSignal, ResetSignal)):
+        pass
+    elif isinstance(value, Slice):
+        _check_lhs(value.value)
+    elif isinstance(value, Part):
+        _check_lhs(value.value)
+        _check_rhs(value.offset)
+    elif isinstance(value, Concat):
+        for part in value.parts:
+            _check_lhs(part)
+    elif isinstance(value, SwitchValue):
+        _check_rhs(value.test)
+        for _patterns, elem in value.cases:
+            _check_lhs(elem)
+    elif isinstance(value, MemoryData._Row):
+        raise ValueError(f"Value {value!r} can only be used in simulator processes")
+    else:
+        raise ValueError(f"Value {value!r} cannot be assigned to")
+
+def _check_rhs(value):
+    if isinstance(value, (Const, Signal, ClockSignal, ResetSignal, Initial, AnyValue)):
+        pass
+    elif isinstance(value, Operator):
+        for op in value.operands:
+            _check_rhs(op)
+    elif isinstance(value, Slice):
+        _check_rhs(value.value)
+    elif isinstance(value, Part):
+        _check_rhs(value.value)
+        _check_rhs(value.offset)
+    elif isinstance(value, Concat):
+        for part in value.parts:
+            _check_rhs(part)
+    elif isinstance(value, SwitchValue):
+        _check_rhs(value.test)
+        for _patterns, elem in value.cases:
+            _check_rhs(elem)
+    elif isinstance(value, MemoryData._Row):
+        raise ValueError(f"Value {value!r} can only be used in simulator processes")
+    else:
+        assert False # :nocov:
 
 
 class _ModuleBuilderProxy:
@@ -144,9 +140,9 @@ def __init__(self, builder, depth):
     def __getattr__(self, name):
         if name in ("comb", "sync"):
             raise AttributeError("'{}' object has no attribute '{}'; did you mean 'd.{}'?"
-                                 .format(type(self).__name__, name, name))
+                                 .format(type(self).__qualname__, name, name))
         raise AttributeError("'{}' object has no attribute '{}'"
-                             .format(type(self).__name__, name))
+                             .format(type(self).__qualname__, name))
 
 
 class _ModuleBuilderSubmodules:
@@ -632,16 +628,27 @@ def _add_statement(self, assigns, domain, depth):
 
             stmt._MustUse__used = True
 
-            visitor = _Visitor()
-            visitor.visit_stmt(stmt)
-            for signal in visitor.driven_signals:
-                if signal not in self._driving:
-                    self._driving[signal] = domain
-                elif self._driving[signal] != domain:
-                    cd_curr = self._driving[signal]
-                    raise SyntaxError(
-                        f"Driver-driver conflict: trying to drive {signal!r} from d.{domain}, but it is "
-                        f"already driven from d.{cd_curr}")
+            _check_stmt(stmt)
+
+            lhs_masks = LHSMaskCollector()
+            # This is an opportunistic early check — not much harm skipping it, since
+            # the whole-design check will be later done in NIR emitter.
+            if not isinstance(stmt, _LateBoundStatement):
+                lhs_masks.visit_stmt(stmt)
+
+            for sig, mask in lhs_masks.masks():
+                if sig not in self._driving:
+                    self._driving[sig] = [None] * len(sig)
+                sig_domain = self._driving[sig]
+                for bit in range(len(sig)):
+                    if not (mask & (1 << bit)):
+                        continue
+                    if sig_domain[bit] is None:
+                        sig_domain[bit] = domain
+                    if sig_domain[bit] != domain:
+                        raise SyntaxError(
+                            f"Driver-driver conflict: trying to drive {sig!r} bit {bit} from d.{domain}, but it is "
+                            f"already driven from d.{sig_domain[bit]}")
 
             self._statements.setdefault(domain, []).append(stmt)
 
diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py
index f24cd2e41..90ddb8a48 100644
--- a/amaranth/hdl/_ir.py
+++ b/amaranth/hdl/_ir.py
@@ -728,17 +728,40 @@ def __init__(self, module_idx: int, signal: _ast.Signal,
         self.src_loc = src_loc
         self.assignments = []
 
-    def emit_value(self, builder):
+    def emit_value(self, builder, chunk_start, chunk_end):
+        chunk_len = chunk_end - chunk_start
         if self.domain is None:
             init = _ast.Const(self.signal.init, len(self.signal))
             default, _signed = builder.emit_rhs(self.module_idx, init)
         else:
             default = builder.emit_signal(self.signal)
-        if len(self.assignments) == 1:
-            assign, = self.assignments
-            if assign.cond == 1 and assign.start == 0 and len(assign.value) == len(default):
-                return assign.value
-        cell = _nir.AssignmentList(self.module_idx, default=default, assignments=self.assignments,
+        default = default[chunk_start:chunk_end]
+        assignments = []
+        for assign in self.assignments:
+            if assign.start >= chunk_end:
+                continue
+            if assign.start + len(assign.value) <= chunk_start:
+                continue
+            if assign.cond == 1 and assign.start == chunk_start and len(assign.value) == chunk_len and len(assignments) == 0:
+                default = assign.value
+            else:
+                if assign.start < chunk_start:
+                    start = 0
+                    value = assign.value[chunk_start - assign.start:]
+                else:
+                    start = assign.start - chunk_start
+                    value = assign.value
+                if start + len(value) > chunk_len:
+                    value = value[:chunk_len - start]
+                assignments.append(_nir.Assignment(
+                    cond=assign.cond,
+                    start=start,
+                    value=value,
+                    src_loc=assign.src_loc
+                ))
+        if len(assignments) == 0:
+            return default
+        cell = _nir.AssignmentList(self.module_idx, default=default, assignments=assignments,
                                    src_loc=self.signal.src_loc)
         return builder.netlist.add_value_cell(len(default), cell)
 
@@ -748,6 +771,7 @@ def __init__(self, netlist: _nir.Netlist, design: Design, *, all_undef_to_ff=Fal
         self.netlist = netlist
         self.design = design
         self.all_undef_to_ff = all_undef_to_ff
+        # SignalDict from Signal to dict from (module index, ClockDomain | None) to NetlistDriver
         self.drivers = _ast.SignalDict()
         self.io_ports: dict[_ast.IOPort, int] = {}
         self.rhs_cache: dict[int, Tuple[_nir.Value, bool, _ast.Value]] = {}
@@ -1064,24 +1088,13 @@ def connect(self, lhs: _nir.Value, rhs: _nir.Value, *, src_loc):
     def emit_assign(self, module_idx: int, cd: "_cd.ClockDomain | None", lhs: _ast.Value, lhs_start: int, rhs: _nir.Value, cond: _nir.Net, *, src_loc):
         # Assign rhs to lhs[lhs_start:lhs_start+len(rhs)]
         if isinstance(lhs, _ast.Signal):
-            if lhs in self.drivers:
-                driver = self.drivers[lhs]
-                if driver.domain is not cd:
-                    domain_name = cd.name if cd is not None else "comb"
-                    other_domain_name = driver.domain.name if driver.domain is not None else "comb"
-                    raise _ir.DriverConflict(
-                        f"Signal {lhs!r} driven from domain {domain_name} at {src_loc[0]}:{src_loc[1]} and domain "
-                        f"{other_domain_name} at {driver.src_loc[0]}:{driver.src_loc[1]}")
-                if driver.module_idx != module_idx:
-                    mod_name = ".".join(self.netlist.modules[module_idx].name or ("<toplevel>",))
-                    other_mod_name = \
-                        ".".join(self.netlist.modules[driver.module_idx].name or ("<toplevel>",))
-                    raise _ir.DriverConflict(
-                        f"Signal {lhs!r} driven from module {mod_name} at {src_loc[0]}:{src_loc[1]} and "
-                        f"module {other_mod_name} at {driver.src_loc[0]}:{driver.src_loc[1]}")
+            sig_drivers = self.drivers.setdefault(lhs, {})
+            key = (module_idx, cd)
+            if key in sig_drivers:
+                driver = sig_drivers[key]
             else:
                 driver = NetlistDriver(module_idx, lhs, domain=cd, src_loc=src_loc)
-                self.drivers[lhs] = driver
+                sig_drivers[key] = driver
             driver.assignments.append(_nir.Assignment(cond=cond, start=lhs_start, value=rhs,
                                                       src_loc=src_loc))
         elif isinstance(lhs, _ast.Slice):
@@ -1425,43 +1438,104 @@ def emit_format(path, fmt):
             self.netlist.signal_fields[signal] = fields
 
     def emit_drivers(self):
-        for driver in self.drivers.values():
-            if (driver.domain is not None and
-                    driver.domain.rst is not None and
-                    not driver.domain.async_reset and
-                    not driver.signal.reset_less):
-                cond = self.emit_matches(driver.module_idx,
-                                         self.emit_signal(driver.domain.rst),
-                                         ("1",),
-                                         src_loc=driver.domain.rst.src_loc)
-                cond, = self.emit_priority_match(driver.module_idx, _nir.Net.from_const(1),
-                                                 _nir.Value(cond),
-                                                 src_loc=driver.domain.rst.src_loc)
-                init = _nir.Value.from_const(driver.signal.init, len(driver.signal))
-                driver.assignments.append(_nir.Assignment(cond=cond, start=0,
-                                                         value=init, src_loc=driver.signal.src_loc))
-            value = driver.emit_value(self)
-            if driver.domain is not None:
-                clk, = self.emit_signal(driver.domain.clk)
-                if driver.domain.rst is not None and driver.domain.async_reset and not driver.signal.reset_less:
-                    arst, = self.emit_signal(driver.domain.rst)
+        for sig, sig_drivers in self.drivers.items():
+            driven_bits = [None] * len(sig)
+            for driver in sig_drivers.values():
+                lhs = self.emit_signal(driver.signal)
+                if len(sig_drivers) == 1 and all(net not in self.netlist.connections for net in lhs):
+                    # If the signal is only assigned from one (module, clock domain) pair, and is
+                    # also not driven by any instance, extend this driver to cover all bits of
+                    # the signal for nicer netlist output.
+                    driver_mask = (1 << len(sig)) - 1
+                    driver_bit_start = 0
+                    driver_bit_stop = len(sig)
                 else:
-                    arst = _nir.Net.from_const(0)
-                cell = _nir.FlipFlop(driver.module_idx,
-                    data=value,
-                    init=driver.signal.init,
-                    clk=clk,
-                    clk_edge=driver.domain.clk_edge,
-                    arst=arst,
-                    attributes=driver.signal.attrs,
-                    src_loc=driver.signal.src_loc,
-                )
-                value = self.netlist.add_value_cell(len(value), cell)
-            if driver.assignments:
-                src_loc = driver.assignments[0].src_loc
-            else:
-                src_loc = driver.signal.src_loc
-            self.connect(self.emit_signal(driver.signal), value, src_loc=src_loc)
+                    # Otherwise, per-bit assignment it is.
+                    driver_mask = 0
+                    driver_bit_start = len(sig)
+                    driver_bit_stop = 0
+                    for assign in driver.assignments:
+                        for bit in range(assign.start, assign.start + len(assign.value)):
+                            driver_mask |= 1 << bit
+                            # The conflict would be caught by connect anyway, but we can have
+                            # a slightly better error message this way (showing the exact colliding
+                            # domains)
+                            if driven_bits[bit] is not None:
+                                other_module_idx, other_domain, other_src_loc = driven_bits[bit]
+                                if other_domain != driver.domain:
+                                    domain_name = driver.domain.name if driver.domain is not None else "comb"
+                                    other_domain_name = other_domain.name if other_domain is not None else "comb"
+                                    raise _ir.DriverConflict(
+                                        f"Signal {sig!r} bit {bit} driven from domain {domain_name} at "
+                                        f"{assign.src_loc[0]}:{assign.src_loc[1]} and domain "
+                                        f"{other_domain_name} at {other_src_loc[0]}:{other_src_loc[1]}")
+                                if other_module_idx != driver.module_idx:
+                                    mod_name = ".".join(self.netlist.modules[driver.module_idx].name or ("<toplevel>",))
+                                    other_mod_name = \
+                                        ".".join(self.netlist.modules[other_module_idx].name or ("<toplevel>",))
+                                    raise _ir.DriverConflict(
+                                        f"Signal {sig!r} bit {bit} driven from module {mod_name} at "
+                                        f"{assign.src_loc[0]}:{assign.src_loc[1]} and "
+                                        f"module {other_mod_name} at "
+                                        f"{other_src_loc[0]}:{other_src_loc[1]}")
+                            else:
+                                driven_bits[bit] = (driver.module_idx, driver.domain, assign.src_loc)
+
+                driver_chunks = []
+                pos = 0
+                while pos < len(sig):
+                    if driver_mask & 1 << pos:
+                        end_pos = pos
+                        while driver_mask & 1 << end_pos:
+                            end_pos += 1
+                        driver_chunks.append((pos, end_pos))
+                        pos = end_pos
+                    else:
+                        pos += 1
+
+
+                if (driver.domain is not None and
+                        driver.domain.rst is not None and
+                        not driver.domain.async_reset and
+                        not driver.signal.reset_less):
+                    cond = self.emit_matches(driver.module_idx,
+                                            self.emit_signal(driver.domain.rst),
+                                            ("1",),
+                                            src_loc=driver.domain.rst.src_loc)
+                    cond, = self.emit_priority_match(driver.module_idx, _nir.Net.from_const(1),
+                                                    _nir.Value(cond),
+                                                    src_loc=driver.domain.rst.src_loc)
+                    init = _nir.Value.from_const(driver.signal.init, len(driver.signal))
+                    driver.assignments.append(_nir.Assignment(cond=cond, start=0,
+                                                            value=init, src_loc=driver.signal.src_loc))
+
+                for chunk_start, chunk_end in driver_chunks:
+                    chunk_len = chunk_end - chunk_start
+                    chunk_mask = (1 << chunk_len) - 1
+
+                    value = driver.emit_value(self, chunk_start, chunk_end)
+                    if driver.domain is not None:
+                        clk, = self.emit_signal(driver.domain.clk)
+                        if driver.domain.rst is not None and driver.domain.async_reset and not driver.signal.reset_less:
+                            arst, = self.emit_signal(driver.domain.rst)
+                        else:
+                            arst = _nir.Net.from_const(0)
+                        cell = _nir.FlipFlop(driver.module_idx,
+                            data=value,
+                            init=(driver.signal.init >> chunk_start) & chunk_mask,
+                            clk=clk,
+                            clk_edge=driver.domain.clk_edge,
+                            arst=arst,
+                            attributes=driver.signal.attrs,
+                            src_loc=driver.signal.src_loc,
+                        )
+                        value = self.netlist.add_value_cell(len(value), cell)
+                    if driver.assignments:
+                        src_loc = driver.assignments[0].src_loc
+                    else:
+                        src_loc = driver.signal.src_loc
+
+                    self.connect(lhs[chunk_start:chunk_end], value, src_loc=src_loc)
 
     def emit_undef_ff(self):
         # Connect all completely undriven signals to flip-flops with const-0 clock. This is used
diff --git a/amaranth/hdl/_mem.py b/amaranth/hdl/_mem.py
index add1a896f..8a2dd5504 100644
--- a/amaranth/hdl/_mem.py
+++ b/amaranth/hdl/_mem.py
@@ -4,6 +4,7 @@
 
 from .. import tracer
 from ._ast import *
+from ._ast import _get_init_value
 from ._ir import Elaboratable, Fragment, AlreadyElaborated
 from ..utils import ceil_log2
 from .._utils import deprecated, final
@@ -106,10 +107,11 @@ def __setitem__(self, index, value):
                 for actual_index, actual_value in zip(indices, value):
                     self[actual_index] = actual_value
             else:
+                raw = _get_init_value(value, self._shape, "memory")
                 if isinstance(self._shape, ShapeCastable):
-                    self._raw[index] = Const.cast(Const(value, self._shape)).value
+                    self._raw[index] = raw
                 else:
-                    value = operator.index(value)
+                    value = raw
                     # self._raw[index] assigned by the following line
                 self._elems[index] = value
 
diff --git a/amaranth/hdl/_nir.py b/amaranth/hdl/_nir.py
index 7f9c26867..c15247a1a 100644
--- a/amaranth/hdl/_nir.py
+++ b/amaranth/hdl/_nir.py
@@ -465,7 +465,7 @@ def traverse(net):
                     elif isinstance(obj, Operator):
                         obj = f"operator {obj.operator}"
                     else:
-                        obj = f"cell {obj.__class__.__name__}"
+                        obj = f"cell {obj.__class__.__qualname__}"
                     src_loc = "<unknown>:0" if src_loc is None else f"{src_loc[0]}:{src_loc[1]}"
                     msg.append(f"  {src_loc}: {obj} bit {bit}\n")
                 raise CombinationalCycle("".join(msg))
@@ -656,7 +656,7 @@ class Operator(Cell):
 
     The ternary operators are:
 
-    - 'm': multiplexer, first input needs to have width of 1, second and third operand need to have 
+    - 'm': multiplexer, first input needs to have width of 1, second and third operand need to have
       the same width as output; implements arg0 ? arg1 : arg2
 
     Attributes
@@ -921,8 +921,8 @@ def __repr__(self):
     def comb_edges_to(self, bit):
         yield (self.default[bit], self.src_loc)
         for assign in self.assignments:
-            yield (assign.cond, assign.src_loc)
             if bit >= assign.start and bit < assign.start + len(assign.value):
+                yield (assign.cond, assign.src_loc)
                 yield (assign.value[bit - assign.start], assign.src_loc)
 
 
diff --git a/amaranth/hdl/_xfrm.py b/amaranth/hdl/_xfrm.py
index 92d070594..d04bbbad6 100644
--- a/amaranth/hdl/_xfrm.py
+++ b/amaranth/hdl/_xfrm.py
@@ -17,6 +17,7 @@
            "FragmentTransformer",
            "TransformedElaboratable",
            "DomainCollector", "DomainRenamer", "DomainLowerer",
+           "LHSMaskCollector",
            "ResetInserter", "EnableInserter"]
 
 
@@ -601,6 +602,71 @@ def on_fragment(self, fragment):
         return super().on_fragment(fragment)
 
 
+class LHSMaskCollector:
+    def __init__(self):
+        self.lhs = SignalDict()
+
+    def visit_stmt(self, stmt):
+        if type(stmt) is Assign:
+            self.visit_value(stmt.lhs, ~0)
+        elif type(stmt) is Switch:
+            for (_, substmt, _) in stmt.cases:
+                self.visit_stmt(substmt)
+        elif type(stmt) in (Property, Print):
+            pass
+        elif isinstance(stmt, Iterable):
+            for substmt in stmt:
+                self.visit_stmt(substmt)
+        else:
+            assert False # :nocov:
+
+    def visit_value(self, value, mask):
+        if type(value) in (Signal, ClockSignal, ResetSignal):
+            mask &= (1 << len(value)) - 1
+            self.lhs.setdefault(value, 0)
+            self.lhs[value] |= mask
+        elif type(value) is Operator:
+            assert value.operator in ("s", "u")
+            self.visit_value(value.operands[0], mask)
+        elif type(value) is Slice:
+            slice_mask = (1 << value.stop) - (1 << value.start)
+            mask <<= value.start
+            mask &= slice_mask
+            self.visit_value(value.value, mask)
+        elif type(value) is Part:
+            # Could be more accurate, but if you're relying on such details, you're not seeing
+            # the Light of Heaven.
+            self.visit_value(value.value, ~0)
+        elif type(value) is Concat:
+            for part in value.parts:
+                self.visit_value(part, mask)
+                mask >>= len(part)
+        elif type(value) is SwitchValue:
+            for (_, subvalue) in value.cases:
+                self.visit_value(subvalue, mask)
+        else:
+            assert False # :nocov:
+
+    def chunks(self):
+        for signal, mask in self.lhs.items():
+            if mask == (1 << len(signal)) - 1:
+                yield signal, 0, None
+            else:
+                start = 0
+                while start < len(signal):
+                    if ((mask >> start) & 1) == 0:
+                        start += 1
+                    else:
+                        stop = start
+                        while stop < len(signal) and ((mask >> stop) & 1) == 1:
+                            stop += 1
+                        yield (signal, start, stop)
+                        start = stop
+
+    def masks(self):
+        yield from self.lhs.items()
+
+
 class _ControlInserter(FragmentTransformer):
     def __init__(self, controls):
         self.src_loc = None
@@ -615,10 +681,9 @@ def on_fragment(self, fragment):
         for domain, statements in fragment.statements.items():
             if domain == "comb" or domain not in self.controls:
                 continue
-            signals = SignalSet()
-            for stmt in statements:
-                signals |= stmt._lhs_signals()
-            self._insert_control(new_fragment, domain, signals)
+            lhs_masks = LHSMaskCollector()
+            lhs_masks.visit_stmt(statements)
+            self._insert_control(new_fragment, domain, lhs_masks)
         return new_fragment
 
     def _insert_control(self, fragment, domain, signals):
@@ -630,13 +695,20 @@ def __call__(self, value, *, src_loc_at=0):
 
 
 class ResetInserter(_ControlInserter):
-    def _insert_control(self, fragment, domain, signals):
-        stmts = [s.eq(Const(s.init, s.shape())) for s in signals if not s.reset_less]
+    def _insert_control(self, fragment, domain, lhs_masks):
+        stmts = []
+        for signal, start, stop in lhs_masks.chunks():
+            if signal.reset_less:
+                continue
+            if start == 0 and stop is None:
+                stmts.append(signal.eq(Const(signal.init, signal.shape())))
+            else:
+                stmts.append(signal[start:stop].eq(Const(signal.init, signal.shape())[start:stop]))
         fragment.add_statements(domain, Switch(self.controls[domain], [(1, stmts, None)], src_loc=self.src_loc))
 
 
 class EnableInserter(_ControlInserter):
-    def _insert_control(self, fragment, domain, signals):
+    def _insert_control(self, fragment, domain, _lhs_masks):
         if domain in fragment.statements:
             fragment.statements[domain] = _StatementList([Switch(
                 self.controls[domain],
diff --git a/amaranth/lib/cdc.py b/amaranth/lib/cdc.py
index 1afcc7698..9939c383b 100644
--- a/amaranth/lib/cdc.py
+++ b/amaranth/lib/cdc.py
@@ -96,7 +96,7 @@ def elaborate(self, platform):
         if self._max_input_delay is not None:
             raise NotImplementedError("Platform '{}' does not support constraining input delay "
                                       "for FFSynchronizer"
-                                      .format(type(platform).__name__))
+                                      .format(type(platform).__qualname__))
 
         m = Module()
         flops = [Signal(self.i.shape(), name=f"stage{index}",
@@ -170,7 +170,7 @@ def elaborate(self, platform):
         if self._max_input_delay is not None:
             raise NotImplementedError("Platform '{}' does not support constraining input delay "
                                       "for AsyncFFSynchronizer"
-                                      .format(type(platform).__name__))
+                                      .format(type(platform).__qualname__))
 
         m = Module()
         m.domains += ClockDomain("async_ff", async_reset=True, local=True)
diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py
index 623d3274c..87624f0a4 100644
--- a/amaranth/lib/data.py
+++ b/amaranth/lib/data.py
@@ -219,12 +219,16 @@ def const(self, init):
         elif isinstance(init, Sequence):
             iterator = enumerate(init)
         else:
-            raise TypeError("Layout constant initializer must be a mapping or a sequence, not {!r}"
-                            .format(init))
+            raise TypeError(f"Layout constant initializer must be a mapping or a sequence, not "
+                            f"{init!r}")
 
         int_value = 0
         for key, key_value in iterator:
-            field = self[key]
+            try:
+                field = self[key]
+            except KeyError:
+                raise ValueError(f"Layout constant initializer refers to key {key!r}, which is not "
+                                 f"a part of the layout")
             cast_field_shape = Shape.cast(field.shape)
             if isinstance(field.shape, ShapeCastable):
                 key_value = hdl.Const.cast(hdl.Const(key_value, field.shape))
@@ -792,17 +796,36 @@ def __getitem__(self, key):
         :exc:`TypeError`
             If :meth:`.ShapeCastable.__call__` does not return a value or a value-castable object.
         """
-        if isinstance(key, slice):
-            raise TypeError(
-                "View cannot be indexed with a slice; did you mean to call `.as_value()` first?")
         if isinstance(self.__layout, ArrayLayout):
-            if not isinstance(key, (int, Value, ValueCastable)):
+            elem_width = Shape.cast(self.__layout.elem_shape).width
+            if isinstance(key, slice):
+                start, stop, stride = key.indices(self.__layout.length)
+                shape = ArrayLayout(self.__layout.elem_shape, len(range(start, stop, stride)))
+                if stride == 1:
+                    value = self.__target[start * elem_width:stop * elem_width]
+                else:
+                    value = Cat(self.__target[index * elem_width:(index + 1) * elem_width]
+                                for index in range(start, stop, stride))
+            elif isinstance(key, int):
+                if key not in range(-self.__layout.length, self.__layout.length):
+                    raise IndexError(f"Index {key} is out of range for array layout of length "
+                                     f"{self.__layout.length}")
+                if key < 0:
+                    key += self.__layout.length
+                shape = self.__layout.elem_shape
+                value = self.__target[key * elem_width:(key + 1) * elem_width]
+            elif isinstance(key, (Value, ValueCastable)):
+                shape = self.__layout.elem_shape
+                value = self.__target.word_select(key, elem_width)
+            else:
                 raise TypeError(
                     f"View with array layout may only be indexed with an integer or a value, "
                     f"not {key!r}")
-            shape = self.__layout.elem_shape
-            value = self.__target.word_select(key, Shape.cast(self.__layout.elem_shape).width)
         else:
+            if isinstance(key, slice):
+                raise TypeError(
+                    "Non-array view cannot be indexed with a slice; did you mean to call "
+                    "`.as_value()` first?")
             if isinstance(key, (Value, ValueCastable)):
                 raise TypeError(
                     f"Only views with array layout, not {self.__layout!r}, may be indexed with "
@@ -850,6 +873,12 @@ def __getattr__(self, name):
                 f"may only be accessed by indexing")
         return item
 
+    def __len__(self):
+        if not isinstance(self.__layout, ArrayLayout):
+            raise TypeError(
+                f"`len()` can only be used on views of array layout, not {self.__layout!r}")
+        return self.__layout.length
+
     def __eq__(self, other):
         if isinstance(other, View) and self.__layout == other.__layout:
             return self.__target == other.__target
@@ -901,7 +930,7 @@ def __and__(self, other):
     __rxor__ = __and__
 
     def __repr__(self):
-        return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})"
+        return f"{self.__class__.__qualname__}({self.__layout!r}, {self.__target!r})"
 
 
 class Const(ValueCastable):
@@ -1015,16 +1044,38 @@ def __getitem__(self, key):
             :meth:`.ShapeCastable.from_bits`. Usually this will be a :exc:`ValueError`.
         """
         if isinstance(self.__layout, ArrayLayout):
-            if isinstance(key, (Value, ValueCastable)):
+            elem_width = Shape.cast(self.__layout.elem_shape).width
+            if isinstance(key, slice):
+                start, stop, stride = key.indices(self.__layout.length)
+                shape = ArrayLayout(self.__layout.elem_shape, len(range(start, stop, stride)))
+                if stride == 1:
+                    value = (self.__target >> start * elem_width) & ((1 << elem_width * (stop - start)) - 1)
+                else:
+                    value = 0
+                    pos = 0
+                    for index in range(start, stop, stride):
+                        elem_value = (self.__target >> index * elem_width) & ((1 << elem_width) - 1)
+                        value |= elem_value << pos
+                        pos += elem_width
+            elif isinstance(key, int):
+                if key not in range(-self.__layout.length, self.__layout.length):
+                    raise IndexError(f"Index {key} is out of range for array layout of length "
+                                     f"{self.__layout.length}")
+                if key < 0:
+                    key += self.__layout.length
+                shape = self.__layout.elem_shape
+                value = (self.__target >> key * elem_width) & ((1 << elem_width) - 1)
+            elif isinstance(key, (Value, ValueCastable)):
                 return View(self.__layout, self.as_value())[key]
-            if not isinstance(key, int):
+            else:
                 raise TypeError(
                     f"Constant with array layout may only be indexed with an integer or a value, "
                     f"not {key!r}")
-            shape = self.__layout.elem_shape
-            elem_width = Shape.cast(self.__layout.elem_shape).width
-            value = (self.__target >> key * elem_width) & ((1 << elem_width) - 1)
         else:
+            if isinstance(key, slice):
+                raise TypeError(
+                    "Non-array constant cannot be indexed with a slice; did you mean to call "
+                    "`.as_value()` first?")
             if isinstance(key, (Value, ValueCastable)):
                 raise TypeError(
                     f"Only constants with array layout, not {self.__layout!r}, may be indexed with "
@@ -1067,15 +1118,30 @@ def __getattr__(self, name):
                 f"may only be accessed by indexing")
         return item
 
+    def __len__(self):
+        if not isinstance(self.__layout, ArrayLayout):
+            raise TypeError(
+                f"`len()` can only be used on constants of array layout, not {self.__layout!r}")
+        return self.__layout.length
+
     def __eq__(self, other):
         if isinstance(other, View) and self.__layout == other._View__layout:
             return self.as_value() == other._View__target
         elif isinstance(other, Const) and self.__layout == other.__layout:
             return self.__target == other.__target
         else:
+            cause = None
+            if isinstance(other, (dict, list)):
+                try:
+                    other_as_const = self.__layout.const(other)
+                except (TypeError, ValueError) as exc:
+                    cause = exc
+                else:
+                    return self == other_as_const
             raise TypeError(
-                f"Constant with layout {self.__layout!r} can only be compared to another view or "
-                f"constant with the same layout, not {other!r}")
+                f"Constant with layout {self.__layout!r} can only be compared to another view, "
+                f"a constant with the same layout, or a dictionary or a list that can be converted "
+                f"to a constant with the same layout, not {other!r}") from cause
 
     def __ne__(self, other):
         if isinstance(other, View) and self.__layout == other._View__layout:
@@ -1083,9 +1149,18 @@ def __ne__(self, other):
         elif isinstance(other, Const) and self.__layout == other.__layout:
             return self.__target != other.__target
         else:
+            cause = None
+            if isinstance(other, (dict, list)):
+                try:
+                    other_as_const = self.__layout.const(other)
+                except (TypeError, ValueError) as exc:
+                    cause = exc
+                else:
+                    return self != other_as_const
             raise TypeError(
-                f"Constant with layout {self.__layout!r} can only be compared to another view or "
-                f"constant with the same layout, not {other!r}")
+                f"Constant with layout {self.__layout!r} can only be compared to another view, "
+                f"a constant with the same layout, or a dictionary or a list that can be converted "
+                f"to a constant with the same layout, not {other!r}") from cause
 
     def __add__(self, other):
         raise TypeError("Cannot perform arithmetic operations on a lib.data.Const")
@@ -1118,7 +1193,7 @@ def __and__(self, other):
     __rxor__ = __and__
 
     def __repr__(self):
-        return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})"
+        return f"{self.__class__.__qualname__}({self.__layout!r}, {self.__target!r})"
 
 
 class _AggregateMeta(ShapeCastable, type):
@@ -1230,7 +1305,7 @@ def is_subnormal(self):
 
         >>> IEEE754Single.as_shape()
         StructLayout({'fraction': 23, 'exponent': 8, 'sign': 1})
-        >>> Signal(IEEE754Single).as_value().width
+        >>> Signal(IEEE754Single).as_value().shape().width
         32
 
     Instances of this class can be used where :ref:`values <lang-values>` are expected:
diff --git a/amaranth/lib/enum.py b/amaranth/lib/enum.py
index 032f281a7..d6463c3bd 100644
--- a/amaranth/lib/enum.py
+++ b/amaranth/lib/enum.py
@@ -178,7 +178,7 @@ def from_bits(cls, bits):
     def format(cls, value, format_spec):
         if format_spec != "":
             raise ValueError(f"Format specifier {format_spec!r} is not supported for enums")
-        return Format.Enum(value, cls, name=cls.__name__)
+        return Format.Enum(value, cls, name=cls.__qualname__)
 
 
 # In 3.11, Python renamed EnumMeta to EnumType. Like Python itself, we support both for
@@ -310,7 +310,7 @@ def __ne__(self, other):
         return self.target != other.target
 
     def __repr__(self):
-        return f"{type(self).__name__}({self.enum.__name__}, {self.target!r})"
+        return f"{type(self).__qualname__}({self.enum.__qualname__}, {self.target!r})"
 
 
 class FlagView(EnumView):
diff --git a/amaranth/lib/io.py b/amaranth/lib/io.py
index e4ea8b2ec..6eed2bf76 100644
--- a/amaranth/lib/io.py
+++ b/amaranth/lib/io.py
@@ -1,5 +1,6 @@
 import enum
 import operator
+import warnings
 from abc import ABCMeta, abstractmethod
 from collections.abc import Iterable
 
@@ -11,7 +12,7 @@
 
 
 __all__ = [
-    "Direction", "PortLike", "SingleEndedPort", "DifferentialPort",
+    "Direction", "PortLike", "SingleEndedPort", "DifferentialPort", "SimulationPort",
     "Buffer", "FFBuffer", "DDRBuffer",
     "Pin",
 ]
@@ -57,6 +58,12 @@ class PortLike(metaclass=ABCMeta):
         :class:`amaranth.hdl.IOPort` is not an instance of :class:`amaranth.lib.io.PortLike`.
     """
 
+    # TODO(amaranth-0.6): remove
+    def __init_subclass__(cls):
+        if cls.__add__ is PortLike.__add__:
+            warnings.warn(f"{cls.__module__}.{cls.__qualname__} must override the `__add__` method",
+                          DeprecationWarning, stacklevel=2)
+
     @property
     @abstractmethod
     def direction(self):
@@ -108,6 +115,32 @@ def __invert__(self):
         """
         raise NotImplementedError # :nocov:
 
+    # TODO(amaranth-0.6): make abstract
+    # @abstractmethod
+    def __add__(self, other):
+        """Concatenates two library I/O ports of the same type.
+
+        The direction of the resulting port is:
+
+        * The same as the direction of both, if the two ports have the same direction.
+        * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
+        * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port.
+
+        Returns
+        -------
+        :py:`type(self)`
+            A new :py:`type(self)` which contains wires from :py:`self` followed by wires
+            from :py:`other`, preserving their polarity inversion.
+
+        Raises
+        ------
+        :exc:`ValueError`
+            If an input port is concatenated with an output port.
+        :exc:`TypeError`
+            If :py:`self` and :py:`other` have different types.
+        """
+        raise NotImplementedError # :nocov:
+
 
 class SingleEndedPort(PortLike):
     """Represents a single-ended library I/O port.
@@ -124,9 +157,9 @@ class SingleEndedPort(PortLike):
         same length as the width of :py:`io`, and the inversion is specified for individual wires.
     direction : :class:`Direction` or :class:`str`
         Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
-        If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used
-        with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be
-        used with buffers of any direction.
+        If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be
+        used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port
+        can be used with buffers of any direction.
 
     Attributes
     ----------
@@ -176,27 +209,6 @@ def __getitem__(self, index):
                                direction=self._direction)
 
     def __add__(self, other):
-        """Concatenates two single-ended library I/O ports.
-
-        The direction of the resulting port is:
-
-        * The same as the direction of both, if the two ports have the same direction.
-        * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
-        * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port.
-
-        Returns
-        -------
-        :class:`SingleEndedPort`
-            A new :class:`SingleEndedPort` which contains wires from :py:`self` followed by wires
-            from :py:`other`, preserving their polarity inversion.
-
-        Raises
-        ------
-        :exc:`ValueError`
-            If an input port is concatenated with an output port.
-        :exc:`TypeError`
-            If :py:`self` and :py:`other` have incompatible types.
-        """
         if not isinstance(other, SingleEndedPort):
             return NotImplemented
         return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert,
@@ -231,9 +243,9 @@ class DifferentialPort(PortLike):
         individual wires.
     direction : :class:`Direction` or :class:`str`
         Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
-        If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used
-        with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be
-        used with buffers of any direction.
+        If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be
+        used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port
+        can be used with buffers of any direction.
 
     Attributes
     ----------
@@ -293,27 +305,6 @@ def __getitem__(self, index):
                                 direction=self._direction)
 
     def __add__(self, other):
-        """Concatenates two differential library I/O ports.
-
-        The direction of the resulting port is:
-
-        * The same as the direction of both, if the two ports have the same direction.
-        * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
-        * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port.
-
-        Returns
-        -------
-        :class:`DifferentialPort`
-            A new :class:`DifferentialPort` which contains pairs of wires from :py:`self` followed
-            by pairs of wires from :py:`other`, preserving their polarity inversion.
-
-        Raises
-        ------
-        :exc:`ValueError`
-            If an input port is concatenated with an output port.
-        :exc:`TypeError`
-            If :py:`self` and :py:`other` have incompatible types.
-        """
         if not isinstance(other, DifferentialPort):
             return NotImplemented
         return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n),
@@ -331,6 +322,167 @@ def __repr__(self):
                 f"direction={self._direction})")
 
 
+class SimulationPort(PortLike):
+    """Represents a simulation library I/O port.
+
+    Implements the :class:`PortLike` interface.
+
+    Parameters
+    ----------
+    direction : :class:`Direction` or :class:`str`
+        Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
+        If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be
+        used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port
+        can be used with buffers of any direction.
+    width : :class:`int`
+        Width of the port. The width of each of the attributes :py:`i`, :py:`o`, :py:`oe` (whenever
+        present) equals :py:`width`.
+    invert : :class:`bool` or iterable of :class:`bool`
+        Polarity inversion. If the value is a simple :class:`bool`, it specifies inversion for
+        the entire port. If the value is an iterable of :class:`bool`, the iterable must have the
+        same length as the width of :py:`p` and :py:`n`, and the inversion is specified for
+        individual wires.
+    name : :class:`str` or :py:`None`
+        Name of the port. This name is only used to derive the names of the input, output, and
+        output enable signals.
+    src_loc_at : :class:`int`
+        :ref:`Source location <lang-srcloc>`. Used to infer :py:`name` if not specified.
+
+    Attributes
+    ----------
+    i : :class:`Signal`
+        Input signal. Present if :py:`direction in (Input, Bidir)`.
+    o : :class:`Signal`
+        Ouptut signal. Present if :py:`direction in (Output, Bidir)`.
+    oe : :class:`Signal`
+        Output enable signal. Present if :py:`direction in (Output, Bidir)`.
+    invert : :class:`tuple` of :class:`bool`
+        The :py:`invert` parameter, normalized to specify polarity inversion per-wire.
+    direction : :class:`Direction`
+        The :py:`direction` parameter, normalized to the :class:`Direction` enumeration.
+    """
+    def __init__(self, direction, width, *, invert=False, name=None, src_loc_at=0):
+        if name is not None and not isinstance(name, str):
+            raise TypeError(f"Name must be a string, not {name!r}")
+        if name is None:
+            name = tracer.get_var_name(depth=2 + src_loc_at, default="$port")
+
+        if not (isinstance(width, int) and width >= 0):
+            raise TypeError(f"Width must be a non-negative integer, not {width!r}")
+
+        self._direction = Direction(direction)
+
+        self._i = self._o = self._oe = None
+        if self._direction in (Direction.Input, Direction.Bidir):
+            self._i  = Signal(width, name=f"{name}__i")
+        if self._direction in (Direction.Output, Direction.Bidir):
+            self._o  = Signal(width, name=f"{name}__o")
+            self._oe = Signal(width, name=f"{name}__oe",
+                              init=~0 if self._direction is Direction.Output else 0)
+
+        if isinstance(invert, bool):
+            self._invert = (invert,) * width
+        elif isinstance(invert, Iterable):
+            self._invert = tuple(invert)
+            if len(self._invert) != width:
+                raise ValueError(f"Length of 'invert' ({len(self._invert)}) doesn't match "
+                                 f"port width ({width})")
+            if not all(isinstance(item, bool) for item in self._invert):
+                raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
+        else:
+            raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
+
+    @property
+    def i(self):
+        if self._i is None:
+            raise AttributeError(
+                "Simulation port with output direction does not have an input signal")
+        return self._i
+
+    @property
+    def o(self):
+        if self._o is None:
+            raise AttributeError(
+                "Simulation port with input direction does not have an output signal")
+        return self._o
+
+    @property
+    def oe(self):
+        if self._oe is None:
+            raise AttributeError(
+                "Simulation port with input direction does not have an output enable signal")
+        return self._oe
+
+    @property
+    def invert(self):
+        return self._invert
+
+    @property
+    def direction(self):
+        return self._direction
+
+    def __len__(self):
+        if self._direction is Direction.Input:
+            return len(self._i)
+        if self._direction is Direction.Output:
+            assert len(self._o) == len(self._oe)
+            return len(self._o)
+        if self._direction is Direction.Bidir:
+            assert len(self._i) == len(self._o) == len(self._oe)
+            return len(self._i)
+        assert False # :nocov:
+
+    def __getitem__(self, key):
+        result = object.__new__(type(self))
+        result._i  = None if self._i  is None else self._i [key]
+        result._o  = None if self._o  is None else self._o [key]
+        result._oe = None if self._oe is None else self._oe[key]
+        if isinstance(key, slice):
+            result._invert = self._invert[key]
+        else:
+            result._invert = (self._invert[key],)
+        result._direction = self._direction
+        return result
+
+    def __invert__(self):
+        result = object.__new__(type(self))
+        result._i = self._i
+        result._o = self._o
+        result._oe = self._oe
+        result._invert = tuple(not invert for invert in self._invert)
+        result._direction = self._direction
+        return result
+
+    def __add__(self, other):
+        if not isinstance(other, SimulationPort):
+            return NotImplemented
+        direction = self._direction & other._direction
+        result = object.__new__(type(self))
+        result._i  = None if direction is Direction.Output else Cat(self._i,  other._i)
+        result._o  = None if direction is Direction.Input  else Cat(self._o,  other._o)
+        result._oe = None if direction is Direction.Input  else Cat(self._oe, other._oe)
+        result._invert = self._invert + other._invert
+        result._direction = direction
+        return result
+
+    def __repr__(self):
+        parts = []
+        if self._i is not None:
+            parts.append(f"i={self._i!r}")
+        if self._o is not None:
+            parts.append(f"o={self._o!r}")
+        if self._oe is not None:
+            parts.append(f"oe={self._oe!r}")
+        if not any(self._invert):
+            invert = False
+        elif all(self._invert):
+            invert = True
+        else:
+            invert = self._invert
+        return (f"SimulationPort({', '.join(parts)}, invert={invert!r}, "
+                f"direction={self._direction})")
+
+
 class Buffer(wiring.Component):
     """A combinational I/O buffer component.
 
@@ -476,6 +628,18 @@ def elaborate(self, platform):
             else:
                 m.submodules += IOBufferInstance(self._port.p, o=o_inv, oe=self.oe, i=i_inv)
                 m.submodules += IOBufferInstance(self._port.n, o=~o_inv, oe=self.oe)
+        elif isinstance(self._port, SimulationPort):
+            if self.direction is Direction.Bidir:
+                # Loop back `o` if `oe` is asserted. This frees the test harness from having to
+                # provide this functionality itself.
+                for i_inv_bit, oe_bit, o_bit, i_bit in \
+                        zip(i_inv, self._port.oe, self._port.o, self._port.i):
+                    m.d.comb += i_inv_bit.eq(Cat(Mux(oe_bit, o_bit, i_bit)))
+            if self.direction is Direction.Input:
+                m.d.comb += i_inv.eq(self._port.i)
+            if self.direction in (Direction.Output, Direction.Bidir):
+                m.d.comb += self._port.o.eq(o_inv)
+                m.d.comb += self._port.oe.eq(self.oe.replicate(len(self._port)))
         else:
             raise TypeError("Cannot elaborate generic 'Buffer' with port {self._port!r}") # :nocov:
 
@@ -719,6 +883,12 @@ class DDRBuffer(wiring.Component):
 
         This limitation may be lifted in the future.
 
+    .. warning::
+
+        Double data rate I/O buffers are not compatible with :class:`SimulationPort`.
+
+        This limitation may be lifted in the future.
+
     Parameters
     ----------
     direction : :class:`Direction`
@@ -826,6 +996,9 @@ def elaborate(self, platform):
         if hasattr(platform, "get_io_buffer"):
             return platform.get_io_buffer(self)
 
+        if isinstance(self._port, SimulationPort):
+            raise NotImplementedError(f"DDR buffers are not supported in simulation") # :nocov:
+
         raise NotImplementedError(f"DDR buffers are not supported on {platform!r}") # :nocov:
 
 
diff --git a/amaranth/lib/stream.py b/amaranth/lib/stream.py
index 2cbf0d422..a24f25bcb 100644
--- a/amaranth/lib/stream.py
+++ b/amaranth/lib/stream.py
@@ -19,7 +19,9 @@ class Signature(wiring.Signature):
     Parameters
     ----------
     payload_shape : :class:`~.hdl.ShapeLike`
-        Shape of the payload.
+        Shape of the payload member.
+    payload_init : :ref:`constant-castable <lang-constcasting>` object
+        Initial value of the payload member.
     always_valid : :class:`bool`
         Whether the stream has a payload available each cycle.
     always_ready : :class:`bool`
@@ -35,14 +37,15 @@ class Signature(wiring.Signature):
     ready : :py:`In(1)`
         Whether a payload is accepted. If the stream is :py:`always_ready`, :py:`Const(1)`.
     """
-    def __init__(self, payload_shape: ShapeLike, *, always_valid=False, always_ready=False):
+    def __init__(self, payload_shape: ShapeLike, *, payload_init=None,
+                 always_valid=False, always_ready=False):
         Shape.cast(payload_shape)
         self._payload_shape = payload_shape
         self._always_valid = bool(always_valid)
         self._always_ready = bool(always_ready)
 
         super().__init__({
-            "payload": Out(payload_shape),
+            "payload": Out(payload_shape, init=payload_init),
             "valid": Out(1),
             "ready": In(1)
         })
@@ -119,4 +122,4 @@ def p(self):
 
     def __repr__(self):
         return (f"stream.Interface(payload={self.payload!r}, valid={self.valid!r}, "
-                f"ready={self.ready!r})")
\ No newline at end of file
+                f"ready={self.ready!r})")
diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py
index 1bf963e96..1122a625f 100644
--- a/amaranth/lib/wiring.py
+++ b/amaranth/lib/wiring.py
@@ -5,6 +5,7 @@
 
 from .. import tracer
 from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value, ValueCastable
+from ..hdl._dsl import Module
 from ..hdl._ir import Elaboratable
 from .._utils import final
 from .meta import Annotation, InvalidAnnotation
@@ -825,8 +826,8 @@ def is_compliant(self, obj, *, reasons=None, path=("obj",)):
             satisfying the requirements below;
           * for port members, is a :ref:`value-like <lang-valuelike>` object casting to
             a :class:`Signal` or a :class:`Const` whose width and signedness is the same as that
-            of the member, and (in case of a :class:`Signal`) which is not reset-less and whose
-            initial value is that of the member;
+            of the member, and (in case of a :class:`Signal`) whose initial value is that of the
+            member;
           * for signature members, matches the description in the signature as verified by
             :meth:`Signature.is_compliant`.
 
@@ -1198,7 +1199,7 @@ def __repr__(self):
         attrs = ''.join(f", {name}={value!r}"
                         for name, value in self.__dict__.items()
                         if name != "signature")
-        return f'<{type(self).__name__}: {self.signature}{attrs}>'
+        return f'<{type(self).__qualname__}: {self.signature}{attrs}>'
 
 
 # To reduce API surface area `FlippedInterface` is made final. This restriction could be lifted
@@ -1401,6 +1402,9 @@ def connect(m, *args, **kwargs):
     a connection cannot be made.
     """
 
+    if not isinstance(m, Module):
+        raise TypeError(f"The initial argument must be a module, not {m!r}")
+
     objects = {
         **{index:   arg for index,   arg in enumerate(args)},
         **{keyword: arg for keyword, arg in kwargs.items()}
diff --git a/amaranth/rpc.py b/amaranth/rpc.py
index af0f125e0..f8149d1aa 100644
--- a/amaranth/rpc.py
+++ b/amaranth/rpc.py
@@ -3,6 +3,8 @@
 import argparse
 import importlib
 
+from amaranth.lib.wiring import Signature
+
 from .hdl import Signal, Record, Elaboratable
 from .back import rtlil
 
@@ -68,16 +70,18 @@ def _serve_yosys(modules):
 
             try:
                 elaboratable = modules[module_name](*args, **kwargs)
-                ports = []
-                # By convention, any public attribute that is a Signal or a Record is
-                # considered a port.
-                for port_name, port in vars(elaboratable).items():
-                    if not port_name.startswith("_") and isinstance(port, (Signal, Record)):
-                        ports += port._lhs_signals()
+                ports = None
+                if not (hasattr(elaboratable, "signature") and isinstance(elaboratable.signature, Signature)):
+                    ports = []
+                    # By convention, any public attribute that is a Signal or a Record is
+                    # considered a port.
+                    for port_name, port in vars(elaboratable).items():
+                        if not port_name.startswith("_") and isinstance(port, (Signal, Record)):
+                            ports += port._lhs_signals()
                 rtlil_text = rtlil.convert(elaboratable, name=module_name, ports=ports)
-                response = {"frontend": "ilang", "source": rtlil_text}
+                response = {"frontend": "rtlil", "source": rtlil_text}
             except Exception as error:
-                response = {"error": f"{type(error).__name__}: {str(error)}"}
+                response = {"error": f"{type(error).__qualname__}: {str(error)}"}
 
         else:
             return {"error": "Unrecognized method {!r}".format(request["method"])}
diff --git a/amaranth/sim/_base.py b/amaranth/sim/_base.py
index c63b95d81..7e58112a4 100644
--- a/amaranth/sim/_base.py
+++ b/amaranth/sim/_base.py
@@ -23,7 +23,7 @@ class BaseSignalState:
     curr = NotImplemented
     next = NotImplemented
 
-    def update(self, value):
+    def update(self, value, mask=~0):
         raise NotImplementedError # :nocov:
 
 
diff --git a/amaranth/sim/_pyrtl.py b/amaranth/sim/_pyrtl.py
index be624fa6b..0c94421ee 100644
--- a/amaranth/sim/_pyrtl.py
+++ b/amaranth/sim/_pyrtl.py
@@ -5,7 +5,7 @@
 
 from ..hdl import *
 from ..hdl._ast import SignalSet, _StatementList, Property
-from ..hdl._xfrm import ValueVisitor, StatementVisitor
+from ..hdl._xfrm import ValueVisitor, StatementVisitor, LHSMaskCollector
 from ..hdl._mem import MemoryInstance
 from ._base import BaseProcess
 from ._pyeval import value_to_string
@@ -487,19 +487,20 @@ def __call__(self, fragment):
         for domain_name in domains:
             domain_stmts = fragment.statements.get(domain_name, _StatementList())
             domain_process = PyRTLProcess(is_comb=domain_name == "comb")
-            domain_signals = domain_stmts._lhs_signals()
+            lhs_masks = LHSMaskCollector()
+            lhs_masks.visit_stmt(domain_stmts)
 
             if isinstance(fragment, MemoryInstance):
                 for port in fragment._read_ports:
                     if port._domain == domain_name:
-                        domain_signals.update(port._data._lhs_signals())
+                        lhs_masks.visit_value(port._data, ~0)
 
             emitter = _PythonEmitter()
             emitter.append(f"def run():")
             emitter._level += 1
 
             if domain_name == "comb":
-                for signal in domain_signals:
+                for (signal, _) in lhs_masks.masks():
                     signal_index = self.state.get_signal(signal)
                     self.state.slots[signal_index].is_comb = True
                     emitter.append(f"next_{signal_index} = {signal.init}")
@@ -533,7 +534,7 @@ def __call__(self, fragment):
                 if domain.async_reset and domain.rst is not None:
                     self.state.add_signal_waker(domain.rst, edge_waker(domain_process, 1))
 
-                for signal in domain_signals:
+                for (signal, _) in lhs_masks.masks():
                     signal_index = self.state.get_signal(signal)
                     emitter.append(f"next_{signal_index} = slots[{signal_index}].next")
 
@@ -546,7 +547,7 @@ def __call__(self, fragment):
                     emitter.append(f"if {rst}:")
                     with emitter.indent():
                         emitter.append("pass")
-                        for signal in domain_signals:
+                        for (signal, _) in lhs_masks.masks():
                             if not signal.reset_less:
                                 signal_index = self.state.get_signal(signal)
                                 emitter.append(f"next_{signal_index} = {signal.init}")
@@ -592,9 +593,11 @@ def __call__(self, fragment):
 
                             lhs(port._data)(data)
 
-            for signal in domain_signals:
+            for (signal, mask) in lhs_masks.masks():
+                if signal.shape().signed and (mask & 1 << (len(signal) - 1)):
+                    mask |= -1 << len(signal)
                 signal_index = self.state.get_signal(signal)
-                emitter.append(f"slots[{signal_index}].update(next_{signal_index})")
+                emitter.append(f"slots[{signal_index}].update(next_{signal_index}, {mask})")
 
             # There shouldn't be any exceptions raised by the generated code, but if there are
             # (almost certainly due to a bug in the code generator), use this environment variable
diff --git a/amaranth/sim/core.py b/amaranth/sim/core.py
index 5c5039939..2c9bc575a 100644
--- a/amaranth/sim/core.py
+++ b/amaranth/sim/core.py
@@ -132,6 +132,18 @@ def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
 
     @staticmethod
     def _check_function(function, *, kind):
+        if inspect.isasyncgenfunction(function):
+            raise TypeError(
+                f"Cannot add a {kind} {function!r} because it is an async generator function "
+                f"(there is likely a stray `yield` in the function)")
+        if inspect.iscoroutine(function):
+            raise TypeError(
+                f"Cannot add a {kind} {function!r} because it is a coroutine object instead "
+                f"of a function (pass the function itself instead of calling it)")
+        if inspect.isgenerator(function) or inspect.isasyncgen(function):
+            raise TypeError(
+                f"Cannot add a {kind} {function!r} because it is a generator object instead "
+                f"of a function (pass the function itself instead of calling it)")
         if not (inspect.isgeneratorfunction(function) or inspect.iscoroutinefunction(function)):
             raise TypeError(
                 f"Cannot add a {kind} {function!r} because it is not an async function or "
diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py
index 2cc646e6e..ddfb8ad2e 100644
--- a/amaranth/sim/pysim.py
+++ b/amaranth/sim/pysim.py
@@ -269,24 +269,22 @@ def close(self, timestamp):
             self.gtkw_save.treeopen("top")
 
             def traverse_traces(traces):
-                if isinstance(traces, Signal):
-                    for name in self.gtkw_signal_names[traces]:
-                        self.gtkw_save.trace(name)
-                elif isinstance(traces, data.View):
+                if isinstance(traces, data.View):
                     with self.gtkw_save.group("view"):
-                        trace = Value.cast(traces)
+                        traverse_traces(Value.cast(traces))
+                elif isinstance(traces, ValueLike):
+                    trace = Value.cast(traces)
+                    if isinstance(traces, MemoryData._Row):
+                        for name in self.gtkw_memory_names[traces._memory][traces._index]:
+                            self.gtkw_save.trace(name)
+                    else:
                         for trace_signal in trace._rhs_signals():
                             for name in self.gtkw_signal_names[trace_signal]:
                                 self.gtkw_save.trace(name)
-                elif isinstance(traces, ValueLike):
-                    traverse_traces(Value.cast(traces))
                 elif isinstance(traces, MemoryData):
                     for row_names in self.gtkw_memory_names[traces]:
                         for name in row_names:
                             self.gtkw_save.trace(name)
-                elif isinstance(traces, MemoryData._Row):
-                    for name in self.gtkw_memory_names[traces._memory][traces._index]:
-                        self.gtkw_save.trace(name)
                 elif hasattr(traces, "signature") and isinstance(traces.signature, wiring.Signature):
                     with self.gtkw_save.group("interface"):
                         for _, _, member in traces.signature.flatten(traces):
@@ -369,7 +367,8 @@ def add_waker(self, waker):
         assert waker not in self.wakers
         self.wakers.append(waker)
 
-    def update(self, value):
+    def update(self, value, mask=~0):
+        value = (self.next & ~mask) | (value & mask)
         if self.next != value:
             self.next = value
             self.pending.add(self)
@@ -393,10 +392,11 @@ def __init__(self, state, addr):
 
 
 class _PyMemoryState(BaseMemoryState):
-    __slots__ = ("memory", "data", "write_queue", "wakers", "pending")
+    __slots__ = ("memory", "shape", "data", "write_queue", "wakers", "pending")
 
     def __init__(self, memory, pending):
         self.memory  = memory
+        self.shape   = Shape.cast(memory.shape)
         self.pending = pending
         self.wakers  = list()
         self.reset()
@@ -420,6 +420,11 @@ def write(self, addr, value, mask=None):
                 self.write_queue[addr] = self.data[addr]
             if mask is not None:
                 value = (value & mask) | (self.write_queue[addr] & ~mask)
+            if self.shape.signed:
+                if value & (1 << (self.shape.width - 1)):
+                    value |= -1 << (self.shape.width)
+                else:
+                    value &= (1 << (self.shape.width)) - 1
             self.write_queue[addr] = value
             self.pending.add(self)
 
diff --git a/amaranth/vendor/_altera.py b/amaranth/vendor/_altera.py
index 8da033e96..679a48527 100644
--- a/amaranth/vendor/_altera.py
+++ b/amaranth/vendor/_altera.py
@@ -222,7 +222,7 @@ class AlteraPlatform(TemplatedPlatform):
         * ``verbose``: enables logging of informational messages to standard error.
         * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
         * ``synth_opts``: adds options for ``synth_intel_alm`` Yosys command.
-        * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
+        * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
         * ``script_after_synth``: inserts commands after ``synth_intel_alm`` in Yosys script.
         * ``yosys_opts``: adds extra options for ``yosys``.
         * ``nextpnr_opts``: adds extra options for ``nextpnr-mistral``.
@@ -310,12 +310,11 @@ class AlteraPlatform(TemplatedPlatform):
             {{get_override("add_settings")|default("# (add_settings placeholder)")}}
         """,
         "{{name}}.sdc": r"""
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}]
-                {% else -%}
-                    create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("|")|tcl_quote}}]
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("|")|tcl_quote}}]
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}]
             {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """,
@@ -374,9 +373,9 @@ class AlteraPlatform(TemplatedPlatform):
                 read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
             {% endfor %}
             {% for file in platform.iter_files(".il") -%}
-                read_ilang {{file}}
+                read_rtlil {{file}}
             {% endfor %}
-            read_ilang {{name}}.il
+            read_rtlil {{name}}.il
             {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
             synth_intel_alm {{get_override("synth_opts")|options}} -top {{name}}
             {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
diff --git a/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py
index 11c4fbf7e..fa063c034 100644
--- a/amaranth/vendor/_gowin.py
+++ b/amaranth/vendor/_gowin.py
@@ -376,9 +376,9 @@ def _osc_div(self):
                 read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
             {% endfor %}
             {% for file in platform.iter_files(".il") -%}
-                read_ilang {{file}}
+                read_rtlil {{file}}
             {% endfor %}
-            read_ilang {{name}}.il
+            read_rtlil {{name}}.il
             {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
             synth_gowin {{get_override("synth_opts")|options}} -top {{name}} -json {{name}}.syn.json
             {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
@@ -437,12 +437,18 @@ def _osc_div(self):
             file delete -force {{name}}.fs
             file copy -force impl/pnr/project.fs {{name}}.fs
         """,
+        # Gowin is using neither Tcl nor the Synopsys code to parse SDC files, so the grammar
+        # deviates from the usual (eg. no quotes, no nested braces).
         "{{name}}.sdc": r"""
         // {{autogenerated}}
-        {% for net_signal,port_signal,frequency in platform.iter_clock_constraints() -%}
-            create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}]
+        {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+            create_clock -name {{ "{" }}{{signal.name}}{{ "}" }} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}]
         {% endfor %}
-        {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
+        {% for port, frequency in platform.iter_port_clock_constraints() -%}
+            create_clock -name {{ "{" }}{{port.name}}{{ "}" }} -period {{1000000000/frequency}} [get_ports
+            {{ "{" }}{{port.name}}{{ "}" }}]
+        {% endfor %}
+        {{get_override("add_constraints")|default("// (add_constraints placeholder)")}}
         """,
     }
     _gowin_command_templates = [
@@ -529,10 +535,14 @@ def create_missing_domain(self, name):
                                              o_OSCOUT=clk_i)
 
             else:
-                clk_i = self.request(self.default_clk).i
+                clk_io = self.request(self.default_clk, dir="-")
+                m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
+                clk_i = clk_buf.i
 
             if self.default_rst is not None:
-                rst_i = self.request(self.default_rst).i
+                rst_io = self.request(self.default_rst, dir="-")
+                m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
+                rst_i = rst_buf.i
             else:
                 rst_i = Const(0)
 
diff --git a/amaranth/vendor/_lattice.py b/amaranth/vendor/_lattice.py
index 834926f4d..04650dd61 100644
--- a/amaranth/vendor/_lattice.py
+++ b/amaranth/vendor/_lattice.py
@@ -319,7 +319,7 @@ class LatticePlatform(TemplatedPlatform):
         * ``verbose``: enables logging of informational messages to standard error.
         * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
         * ``synth_opts``: adds options for ``synth_<family>`` Yosys command.
-        * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
+        * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
         * ``script_after_synth``: inserts commands after ``synth_<family>`` in Yosys script.
         * ``yosys_opts``: adds extra options for ``yosys``.
         * ``nextpnr_opts``: adds extra options for ``nextpnr-<family>``.
@@ -348,7 +348,7 @@ class LatticePlatform(TemplatedPlatform):
         * ``verbose``: enables logging of informational messages to standard error.
         * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
         * ``synth_opts``: adds options for ``synth_nexus`` Yosys command.
-        * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
+        * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
         * ``script_after_synth``: inserts commands after ``synth_nexus`` in Yosys script.
         * ``yosys_opts``: adds extra options for ``yosys``.
         * ``nextpnr_opts``: adds extra options for ``nextpnr-nexus``.
@@ -474,9 +474,9 @@ class LatticePlatform(TemplatedPlatform):
                 read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
             {% endfor %}
             {% for file in platform.iter_files(".il") -%}
-                read_ilang {{file}}
+                read_rtlil {{file}}
             {% endfor %}
-            read_ilang {{name}}.il
+            read_rtlil {{name}}.il
             {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
             {% if platform.family == "ecp5" %}
                 synth_ecp5 {{get_override("synth_opts")|options}} -top {{name}}
@@ -497,12 +497,11 @@ class LatticePlatform(TemplatedPlatform):
                     {%- for key, value in attrs.items() %} {{key}}={{value}}{% endfor %};
                 {% endif %}
             {% endfor %}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    FREQUENCY PORT "{{port_signal.name}}" {{frequency}} HZ;
-                {% else -%}
-                    FREQUENCY NET "{{net_signal|hierarchy(".")}}" {{frequency}} HZ;
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                FREQUENCY NET "{{signals|hierarchy(".")}}" {{frequency}} HZ;
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                FREQUENCY PORT "{{port.name}}" {{frequency}} HZ;
             {% endfor %}
             {{get_override("add_preferences")|default("# (add_preferences placeholder)")}}
         """
@@ -567,9 +566,9 @@ class LatticePlatform(TemplatedPlatform):
                 read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
             {% endfor %}
             {% for file in platform.iter_files(".il") -%}
-                read_ilang {{file}}
+                read_rtlil {{file}}
             {% endfor %}
-            read_ilang {{name}}.il
+            read_rtlil {{name}}.il
             delete w:$verilog_initial_trigger
             {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
             synth_nexus {{get_override("synth_opts")|options}} -top {{name}}
@@ -584,12 +583,11 @@ class LatticePlatform(TemplatedPlatform):
                 ldc_set_port -iobuf {{ '{' }}{%- for key, value in attrs.items() %}{{key}}={{value}} {% endfor %}{{ '}' }} {{'['}}get_ports {{port_name}}{{']'}}
                 {% endif %}
             {% endfor %}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}]
-                {% else -%}
-                    create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}]
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}]
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}]
             {% endfor %}
             {{get_override("add_preferences")|default("# (add_preferences placeholder)")}}
         """
@@ -684,12 +682,11 @@ class LatticePlatform(TemplatedPlatform):
         """,
         "{{name}}.sdc": r"""
             set_hierarchy_separator {/}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -name {{port_signal.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote("Diamond")}}]
-                {% else -%}
-                    create_clock -name {{net_signal.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote("Diamond")}}]
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -name {{signal.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote("Diamond")}}]
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -name {{port.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote("Diamond")}}]
             {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """,
@@ -782,12 +779,11 @@ class LatticePlatform(TemplatedPlatform):
         """,
         # Pre-synthesis SDC constraints
         "{{name}}.sdc": r"""
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name}}]
-                {% else -%}
-                    create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")}}]
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}]
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}]
             {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """,
@@ -960,9 +956,14 @@ def create_missing_domain(self, name):
                     o_HFCLKOUT=clk_i,
                 )
             else:
-                clk_i = self.request(self.default_clk).i
+                clk_io = self.request(self.default_clk, dir="-")
+                m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
+                clk_i = clk_buf.i
+
             if self.default_rst is not None:
-                rst_i = self.request(self.default_rst).i
+                rst_io = self.request(self.default_rst, dir="-")
+                m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
+                rst_i = rst_buf.i
             else:
                 rst_i = Const(0)
 
diff --git a/amaranth/vendor/_quicklogic.py b/amaranth/vendor/_quicklogic.py
index 33a490e41..c7809b9a6 100644
--- a/amaranth/vendor/_quicklogic.py
+++ b/amaranth/vendor/_quicklogic.py
@@ -1,6 +1,7 @@
 from abc import abstractmethod
 
 from ..hdl import *
+from ..lib import io
 from ..lib.cdc import ResetSynchronizer
 from ..build import *
 
@@ -71,10 +72,11 @@ class QuicklogicPlatform(TemplatedPlatform):
         """,
         "{{name}}.sdc": r"""
             # {{autogenerated}}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -period {{100000000/frequency}} {{port_signal.name|ascii_escape}}
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -period {{100000000/frequency}} {{signal.name|ascii_escape}}
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -period {{100000000/frequency}} {{port.name|ascii_escape}}
             {% endfor %}
         """
     }
@@ -171,10 +173,14 @@ def create_missing_domain(self, name):
                                          o_A=sys_clk0,
                                          o_Z=clk_i)
             else:
-                clk_i = self.request(self.default_clk).i
+                clk_io = self.request(self.default_clk, dir="-")
+                m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
+                clk_i = clk_buf.i
 
             if self.default_rst is not None:
-                rst_i = self.request(self.default_rst).i
+                rst_io = self.request(self.default_rst, dir="-")
+                m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
+                rst_i = rst_buf.i
             else:
                 rst_i = Const(0)
 
diff --git a/amaranth/vendor/_siliconblue.py b/amaranth/vendor/_siliconblue.py
index 0db838088..0d179c0ec 100644
--- a/amaranth/vendor/_siliconblue.py
+++ b/amaranth/vendor/_siliconblue.py
@@ -26,7 +26,7 @@ class SiliconBluePlatform(TemplatedPlatform):
         * ``verbose``: enables logging of informational messages to standard error.
         * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
         * ``synth_opts``: adds options for ``synth_ice40`` Yosys command.
-        * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
+        * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
         * ``script_after_synth``: inserts commands after ``synth_ice40`` in Yosys script.
         * ``yosys_opts``: adds extra options for ``yosys``.
         * ``nextpnr_opts``: adds extra options for ``nextpnr-ice40``.
@@ -121,9 +121,9 @@ class SiliconBluePlatform(TemplatedPlatform):
                 read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
             {% endfor %}
             {% for file in platform.iter_files(".il") -%}
-                read_ilang {{file}}
+                read_rtlil {{file}}
             {% endfor %}
-            read_ilang {{name}}.il
+            read_rtlil {{name}}.il
             {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
             synth_ice40 {{get_override("synth_opts")|options}} -top {{name}}
             {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
@@ -134,9 +134,12 @@ class SiliconBluePlatform(TemplatedPlatform):
             {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%}
                 set_io {{port_name}} {{pin_name}}
             {% endfor %}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                set_frequency {{net_signal|hierarchy(".")}} {{frequency/1000000}}
-            {% endfor%}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                set_frequency {{signal|hierarchy(".")}} {{frequency/1000000}}
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                set_frequency {{port.name}} {{frequency/1000000}}
+            {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """,
     }
@@ -242,12 +245,11 @@ class SiliconBluePlatform(TemplatedPlatform):
         "{{name}}.sdc": r"""
             # {{autogenerated}}
             set_hierarchy_separator {/}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}]
-                {% else -%}
-                    create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}]
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}]
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}]
             {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """,
@@ -391,11 +393,15 @@ def create_missing_domain(self, name):
                 delay = int(100e-6 * self.default_clk_frequency)
             # User-defined clock signal.
             else:
-                clk_i = self.request(self.default_clk).i
+                clk_io = self.request(self.default_clk, dir="-")
+                m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
+                clk_i = clk_buf.i
                 delay = int(15e-6 * self.default_clk_frequency)
 
             if self.default_rst is not None:
-                rst_i = self.request(self.default_rst).i
+                rst_io = self.request(self.default_rst, dir="-")
+                m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
+                rst_i = rst_buf.i
             else:
                 rst_i = Const(0)
 
@@ -562,6 +568,10 @@ def get_io_buffer(self, buffer):
                 raise TypeError("iCE40 does not support bidirectional differential ports")
             elif buffer.direction is io.Direction.Output:
                 m = Module()
+                # Note that the non-inverting output pin is not driven the same way as a regular
+                # output pin. The inverter introduces a delay, so for a non-inverting output pin,
+                # an identical delay is introduced by instantiating a LUT. This makes the waveform
+                # perfectly symmetric in the xdr=0 case.
                 invert_lut = isinstance(buffer, io.Buffer)
                 m.submodules.p = self._get_io_buffer_single(buffer, port_p, invert_lut=invert_lut)
                 m.submodules.n = self._get_io_buffer_single(buffer, port_n, invert_lut=invert_lut)
@@ -572,7 +582,7 @@ def get_io_buffer(self, buffer):
                 # differs between LP/HX and UP series:
                 #  * for LP/HX, z=0 is DPxxB   (B is non-inverting, A is inverting)
                 #  * for UP,    z=0 is IOB_xxA (A is non-inverting, B is inverting)
-                return self._get_io_buffer_single(buffer, port_p, invert_lut=invert_lut)
+                return self._get_io_buffer_single(buffer, port_p)
             else:
                 assert False # :nocov:
         elif isinstance(buffer.port, io.SingleEndedPort):
diff --git a/amaranth/vendor/_xilinx.py b/amaranth/vendor/_xilinx.py
index 9a44c6253..1bc454e61 100644
--- a/amaranth/vendor/_xilinx.py
+++ b/amaranth/vendor/_xilinx.py
@@ -647,12 +647,11 @@ def vendor_toolchain(self):
                     set_property {{attr_name}} {{attr_value|tcl_quote}} [get_ports {{port_name|tcl_quote}}]
                 {% endfor %}
             {% endfor %}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is not none -%}
-                    create_clock -name {{port_signal.name|ascii_escape}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}]
-                {% else -%}
-                    create_clock -name {{net_signal.name|ascii_escape}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}]
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -name {{signal.name|ascii_escape}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}]
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -name {{port.name|ascii_escape}} -period {{1000000000/frequency}} [get_nets {{port|hierarchy("/")|tcl_quote}}]
             {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """
@@ -723,9 +722,13 @@ def vendor_toolchain(self):
                     NET "{{port_name}}" {{attr_name}}={{attr_value}};
                 {% endfor %}
             {% endfor %}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                NET "{{net_signal|hierarchy("/")}}" TNM_NET="PRD{{net_signal|hierarchy("/")}}";
-                TIMESPEC "TS{{net_signal|hierarchy("__")}}"=PERIOD "PRD{{net_signal|hierarchy("/")}}" {{1000000000/frequency}} ns HIGH 50%;
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                NET "{{signal|hierarchy("/")}}" TNM_NET="PRD{{signal|hierarchy("/")}}";
+                TIMESPEC "TS{{signal|hierarchy("__")}}"=PERIOD "PRD{{signal|hierarchy("/")}}" {{1000000000/frequency}} ns HIGH 50%;
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                NET "{{port|hierarchy("/")}}" TNM_NET="PRD{{port|hierarchy("/")}}";
+                TIMESPEC "TS{{port|hierarchy("__")}}"=PERIOD "PRD{{port|hierarchy("/")}}" {{1000000000/frequency}} ns HIGH 50%;
             {% endfor %}
             {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
         """
@@ -848,10 +851,11 @@ def _symbiflow_device(self):
         """,
         "{{name}}.sdc": r"""
             # {{autogenerated}}
-            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
-                {% if port_signal is none -%}
-                    create_clock -period {{1000000000/frequency}} {{net_signal.name|ascii_escape}}
-                {% endif %}
+            {% for signal, frequency in platform.iter_signal_clock_constraints() -%}
+                create_clock -period {{1000000000/frequency}} {{signal.name|ascii_escape}}
+            {% endfor %}
+            {% for port, frequency in platform.iter_port_clock_constraints() -%}
+                create_clock -period {{1000000000/frequency}} {{port.name|ascii_escape}}
             {% endfor %}
         """
     }
@@ -1140,11 +1144,16 @@ def create_missing_domain(self, name):
             return super().create_missing_domain(name)
 
         if name == "sync" and self.default_clk is not None:
-            clk_i = self.request(self.default_clk).i
+            m = Module()
+
+            clk_io = self.request(self.default_clk, dir="-")
+            m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
+            clk_i = clk_buf.i
             if self.default_rst is not None:
-                rst_i = self.request(self.default_rst).i
+                rst_io = self.request(self.default_rst, dir="-")
+                m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
+                rst_i = rst_buf.i
 
-            m = Module()
             ready = Signal()
             m.submodules += Instance(STARTUP_PRIMITIVE[self.family], o_EOS=ready)
             m.domains += ClockDomain("sync", reset_less=self.default_rst is None)
@@ -1173,7 +1182,8 @@ def create_missing_domain(self, name):
 
     def add_clock_constraint(self, clock, frequency):
         super().add_clock_constraint(clock, frequency)
-        clock.attrs["keep"] = "TRUE"
+        if not isinstance(clock, IOPort):
+            clock.attrs["keep"] = "TRUE"
 
     def get_io_buffer(self, buffer):
         if isinstance(buffer, io.Buffer):
@@ -1230,13 +1240,13 @@ def get_ff_sync(self, ff_sync):
                  for index in range(ff_sync._stages)]
         if self.toolchain == "Vivado":
             if ff_sync._max_input_delay is None:
-                flops[0].attrs["amaranth.vivado.false_path"] = "TRUE"
+                Value.cast(flops[0]).attrs["amaranth.vivado.false_path"] = "TRUE"
             else:
-                flops[0].attrs["amaranth.vivado.max_delay"] = str(ff_sync._max_input_delay * 1e9)
+                Value.cast(flops[0]).attrs["amaranth.vivado.max_delay"] = str(ff_sync._max_input_delay * 1e9)
         elif ff_sync._max_input_delay is not None:
             raise NotImplementedError("Platform '{}' does not support constraining input delay "
                                       "for FFSynchronizer"
-                                      .format(type(self).__name__))
+                                      .format(type(self).__qualname__))
         for i, o in zip((ff_sync.i, *flops), flops):
             m.d[ff_sync._o_domain] += o.eq(i)
         m.d.comb += ff_sync.o.eq(flops[-1])
@@ -1255,7 +1265,7 @@ def get_async_ff_sync(self, async_ff_sync):
         flops_d = Signal(async_ff_sync._stages, reset_less=True)
         flops_pre = Signal(reset_less=True)
         for i in range(async_ff_sync._stages):
-            flop = Instance("FDPE", p_INIT=1, o_Q=flops_q[i],
+            flop = Instance("FDPE", p_INIT=Const(1), o_Q=flops_q[i],
                             i_C=ClockSignal(async_ff_sync._o_domain),
                             i_CE=Const(1), i_PRE=flops_pre, i_D=flops_d[i],
                             a_ASYNC_REG="TRUE")
@@ -1284,7 +1294,7 @@ def get_async_ff_sync(self, async_ff_sync):
             elif async_ff_sync._max_input_delay is not None:
                 raise NotImplementedError("Platform '{}' does not support constraining input delay "
                                           "for AsyncFFSynchronizer"
-                                          .format(type(self).__name__))
+                                          .format(type(self).__qualname__))
 
         for i, o in zip((0, *flops_q), flops_d):
             m.d.comb += o.eq(i)
diff --git a/docs/changes.rst b/docs/changes.rst
index 028091636..d2414be52 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -1,7 +1,7 @@
 Changelog
 #########
 
-This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes.
+This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes; versions which do not include notable changes are not listed here.
 
 
 Documentation for past releases
@@ -9,6 +9,11 @@ Documentation for past releases
 
 Documentation for past releases of the Amaranth language and toolchain is available online:
 
+* `Amaranth 0.5.4 <https://amaranth-lang.org/docs/amaranth/v0.5.3/>`_
+* `Amaranth 0.5.3 <https://amaranth-lang.org/docs/amaranth/v0.5.3/>`_
+* `Amaranth 0.5.2 <https://amaranth-lang.org/docs/amaranth/v0.5.2/>`_
+* `Amaranth 0.5.1 <https://amaranth-lang.org/docs/amaranth/v0.5.1/>`_
+* `Amaranth 0.5.0 <https://amaranth-lang.org/docs/amaranth/v0.5.0/>`_
 * `Amaranth 0.4.5 <https://amaranth-lang.org/docs/amaranth/v0.4.5/>`_
 * `Amaranth 0.4.4 <https://amaranth-lang.org/docs/amaranth/v0.4.4/>`_
 * `Amaranth 0.4.3 <https://amaranth-lang.org/docs/amaranth/v0.4.3/>`_
@@ -18,8 +23,75 @@ Documentation for past releases of the Amaranth language and toolchain is availa
 * `Amaranth 0.3 <https://amaranth-lang.org/docs/amaranth/v0.3/>`_
 
 
-Version 0.5
-===========
+Version 0.5.4
+=============
+
+Updated to address deprecations in Yosys 0.48.
+
+
+Version 0.5.3
+=============
+
+
+Language changes
+----------------
+
+* Added: individual bits of the same signal can now be assigned from different modules or domains.
+
+
+Toolchain changes
+-----------------
+
+* Added: the Amaranth RPC server can now elaborate :class:`amaranth.lib.wiring.Component` objects on demand.
+
+
+Version 0.5.2
+=============
+
+
+Standard library changes
+------------------------
+
+.. currentmodule:: amaranth.lib
+
+* Added: constants of :class:`amaranth.lib.data.ArrayLayout` can be indexed with negative integers or slices.
+* Added: :py:`len()` works on constants of :class:`amaranth.lib.data.ArrayLayout`.
+* Added: constants of :class:`amaranth.lib.data.ArrayLayout` are iterable.
+
+
+Platform integration changes
+----------------------------
+
+.. currentmodule:: amaranth.vendor
+
+* Added: :meth:`Platform.request` accepts :py:`dir="-"` for resources with subsignals.
+
+
+Version 0.5.1
+=============
+
+
+Implemented RFCs
+----------------
+
+.. _RFC 69: https://amaranth-lang.org/rfcs/0069-simulation-port.html
+
+* `RFC 69`_: Add a ``lib.io.PortLike`` object usable in simulation
+
+
+Standard library changes
+------------------------
+
+.. currentmodule:: amaranth.lib
+
+* Added: views of :class:`amaranth.lib.data.ArrayLayout` can be indexed with negative integers or slices.
+* Added: :py:`len()` works on views of :class:`amaranth.lib.data.ArrayLayout`.
+* Added: views of :class:`amaranth.lib.data.ArrayLayout` are iterable.
+* Added: :class:`io.SimulationPort`. (`RFC 69`_)
+
+
+Version 0.5.0
+=============
 
 The Migen compatibility layer has been removed.
 
@@ -46,6 +118,7 @@ Apply the following changes to code written against Amaranth 0.4 to migrate it t
 * Update uses of :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` to remove the :py:`run_passive=True` argument. If the code uses :py:`run_passive=False`, ensure it still works with the new behavior.
 * Update uses of :py:`amaranth.utils.log2_int(need_pow2=False)` to :func:`amaranth.utils.ceil_log2`.
 * Update uses of :py:`amaranth.utils.log2_int(need_pow2=True)` to :func:`amaranth.utils.exact_log2`.
+* Replace uses of :py:`a.implies(b)` with `~a | b`.
 
 
 Implemented RFCs
@@ -116,9 +189,10 @@ Language changes
 * Deprecated: :func:`amaranth.utils.log2_int`. (`RFC 17`_)
 * Deprecated: :class:`amaranth.hdl.Memory`. (`RFC 45`_)
 * Deprecated: upwards propagation of clock domains. (`RFC 59`_)
-* Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_)
-* Removed: (deprecated in 0.4) :class:`Repl`. (`RFC 10`_)
-* Removed: (deprecated in 0.4) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`.
+* Deprecated: :meth:`Value.implies`.
+* Removed: (deprecated in 0.4.0) :meth:`Const.normalize`. (`RFC 5`_)
+* Removed: (deprecated in 0.4.0) :class:`Repl`. (`RFC 10`_)
+* Removed: (deprecated in 0.4.0) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`.
 * Removed: assertion names in :class:`Assert`, :class:`Assume` and :class:`Cover`. (`RFC 50`_)
 * Removed: accepting non-subclasses of :class:`Elaboratable` as elaboratables.
 
@@ -137,9 +211,9 @@ Standard library changes
 * Added: :mod:`amaranth.lib.meta`, :class:`amaranth.lib.wiring.ComponentMetadata`. (`RFC 30`_)
 * Added: :mod:`amaranth.lib.stream`. (`RFC 61`_)
 * Deprecated: :mod:`amaranth.lib.coding`. (`RFC 63`_)
-* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
-* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_)
-* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_)
+* Removed: (deprecated in 0.4.0) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
+* Removed: (deprecated in 0.4.0) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_)
+* Removed: (deprecated in 0.4.0) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_)
 
 
 Toolchain changes
@@ -152,8 +226,9 @@ Toolchain changes
 * Changed: :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` always runs the simulation until the given deadline, even when no critical processes or testbenches are present.
 * Deprecated: :py:`Settle` simulation command. (`RFC 27`_)
 * Deprecated: :py:`Simulator.add_sync_process`. (`RFC 27`_)
+* Deprecated: generator-based simulation processes and testbenches. (`RFC 36`_)
 * Deprecated: the :py:`run_passive` argument to :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` has been deprecated, and does nothing.
-* Removed: (deprecated in 0.4) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``.
+* Removed: (deprecated in 0.4.0) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``.
 
 
 Platform integration changes
@@ -166,11 +241,11 @@ Platform integration changes
 * Added: ``build.sh``  begins with ``#!/bin/sh``.
 * Changed: ``IntelPlatform`` renamed to ``AlteraPlatform``.
 * Deprecated: argument :py:`run_script=` in :meth:`BuildPlan.execute_local`.
-* Removed: (deprecated in 0.4) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_)
+* Removed: (deprecated in 0.4.0) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_)
 
 
-Version 0.4
-===========
+Version 0.4.0
+=============
 
 Support has been added for a new and improved way of defining data structures in :mod:`amaranth.lib.data` and component interfaces in :mod:`amaranth.lib.wiring`, as defined in `RFC 1`_ and `RFC 2`_. :class:`Record` has been deprecated. In a departure from the usual policy, to give designers additional time to migrate, :class:`Record` will be removed in Amaranth 0.6 (one release later than normal).
 
diff --git a/docs/guide.rst b/docs/guide.rst
index 71838a96c..4923f0cfe 100644
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -170,6 +170,14 @@ Specifying a shape with a range is convenient for counters, indexes, and all oth
 
    Python ranges are *exclusive* or *half-open*, meaning they do not contain their ``.stop`` element. Because of this, values with shapes cast from a ``range(stop)`` where ``stop`` is a power of 2 are not wide enough to represent ``stop`` itself:
 
+   .. doctest::
+      :hide:
+
+      >>> import warnings
+      >>> _warning_filters_backup = warnings.catch_warnings()
+      >>> _warning_filters_backup.__enter__() # have to do this horrific hack to make it work with `PYTHONWARNINGS=error` :(
+      >>> warnings.simplefilter("default", amaranth.hdl._ast.SyntaxWarning)
+
    .. doctest::
 
       >>> fencepost = C(256, range(256))
@@ -180,6 +188,11 @@ Specifying a shape with a range is convenient for counters, indexes, and all oth
       >>> fencepost.value
       0
 
+   .. doctest::
+      :hide:
+
+      >>> _warning_filters_backup.__exit__()
+
    Amaranth detects uses of :class:`Const` and :class:`Signal` that invoke such an off-by-one error, and emits a diagnostic message.
 
 .. note::
@@ -670,6 +683,14 @@ Python expression Amaranth expression (boolean operands)
 
    When applied to Amaranth boolean values, the ``~`` operator computes negation, and when applied to Python boolean values, the ``not`` operator also computes negation. However, the ``~`` operator applied to Python boolean values produces an unexpected result:
 
+   .. doctest::
+      :hide:
+
+      >>> import warnings
+      >>> _warning_filters_backup = warnings.catch_warnings()
+      >>> _warning_filters_backup.__enter__() # have to do this horrific hack to make it work with `PYTHONWARNINGS=error` :(
+      >>> warnings.simplefilter("ignore", DeprecationWarning)
+
    .. doctest::
 
       >>> ~False
@@ -688,6 +709,11 @@ Python expression Amaranth expression (boolean operands)
       >>> ~use_stb | stb # WRONG! MSB of 2-bit wide OR expression is always 1
       (| (const 2'sd-2) (sig stb))
 
+   .. doctest::
+      :hide:
+
+      >>> _warning_filters_backup.__exit__()
+
    Amaranth automatically detects some cases of misuse of ``~`` and emits a detailed diagnostic message.
 
    .. TODO: this isn't quite reliable, #380
@@ -914,7 +940,7 @@ If the name of a domain is not known upfront, the ``m.d["<domain>"] += ...`` syn
 
 .. _lang-signalgranularity:
 
-Every signal included in the target of an assignment becomes a part of the domain, or equivalently, *driven* by that domain. A signal can be either undriven or driven by exactly one domain; it is an error to add two assignments to the same signal to two different domains:
+Every signal bit included in the target of an assignment becomes a part of the domain, or equivalently, *driven* by that domain. A signal bit can be either undriven or driven by exactly one domain; it is an error to add two assignments to the same signal bit to two different domains:
 
 .. doctest::
 
@@ -923,19 +949,15 @@ Every signal included in the target of an assignment becomes a part of the domai
    >>> m.d.sync += d.eq(0)
    Traceback (most recent call last):
      ...
-   amaranth.hdl.dsl.SyntaxError: Driver-driver conflict: trying to drive (sig d) from d.sync, but it is already driven from d.comb
+   amaranth.hdl.dsl.SyntaxError: Driver-driver conflict: trying to drive (sig d) bit 0 from d.sync, but it is already driven from d.comb
 
-.. note::
-
-   Clearly, Amaranth code that drives a single bit of a signal from two different domains does not describe a meaningful circuit. However, driving two different bits of a signal from two different domains does not inherently cause such a conflict. Would Amaranth accept the following code?
+However, two different bits of a signal can be driven from two different domains without an issue:
 
-   .. code-block::
-
-      e = Signal(2)
-      m.d.comb += e[0].eq(0)
-      m.d.sync += e[1].eq(1)
+.. testcode::
 
-   The answer is no. While this kind of code is occasionally useful, rejecting it greatly simplifies backends, simulators, and analyzers.
+   e = Signal(2)
+   m.d.comb += e[0].eq(1)
+   m.d.sync += e[1].eq(0)
 
 In addition to assignments, :ref:`assertions <lang-assert>` and :ref:`debug prints <lang-print>` can be added using the same syntax.
 
diff --git a/docs/stdlib/io.rst b/docs/stdlib/io.rst
index 3740ccfdd..295fbc867 100644
--- a/docs/stdlib/io.rst
+++ b/docs/stdlib/io.rst
@@ -60,7 +60,8 @@ All of the following examples assume that one of the built-in FPGA platforms is
 
 .. testcode::
 
-    from amaranth.lib import io, wiring
+    from amaranth.sim import Simulator
+    from amaranth.lib import io, wiring, stream
     from amaranth.lib.wiring import In, Out
 
 
@@ -191,6 +192,74 @@ In this example of a `source-synchronous interface <https://en.wikipedia.org/wik
 This component transmits :py:`dout` on each cycle as two halves: the low 8 bits on the rising edge of the data clock, and the high 8 bits on the falling edge of the data clock. The transmission is *edge-aligned*, meaning that the data edges exactly coincide with the clock edges.
 
 
+Simulation
+----------
+
+The Amaranth simulator, :mod:`amaranth.sim`, cannot simulate :ref:`core I/O values <lang-iovalues>` or :ref:`I/O buffer instances <lang-iobufferinstance>` as it only operates on unidirectionally driven two-state wires. This module provides a simulation-only library I/O port, :class:`SimulationPort`, so that components that use library I/O buffers can be tested.
+
+A component that is designed for testing should accept the library I/O ports it will drive as constructor parameters rather than requesting them from the platform directly. Synthesizable designs will instantiate the component with a :class:`SingleEndedPort`, :class:`DifferentialPort`, or a platform-specific library I/O port, while tests will instantiate the component with a :class:`SimulationPort`. Tests are able to inject inputs into the component using :py:`sim_port.i`, capture the outputs of the component via :py:`sim_port.o`, and ensure that the component is driving the outputs at the appropriate times using :py:`sim_port.oe`.
+
+For example, consider a simple serializer that accepts a stream of multi-bit data words and outputs them bit by bit. It can be tested as follows:
+
+.. testcode::
+
+    class OutputSerializer(wiring.Component):
+        data: In(stream.Signature(8))
+
+        def __init__(self, dclk_port, dout_port):
+            self.dclk_port = dclk_port
+            self.dout_port = dout_port
+
+            super().__init__()
+
+        def elaborate(self, platform):
+            m = Module()
+
+            m.submodules.dclk = dclk = io.Buffer("o", self.dclk_port)
+            m.submodules.dout = dout = io.Buffer("o", self.dout_port)
+
+            index = Signal(range(8))
+            m.d.comb += dout.o.eq(self.data.payload.bit_select(index, 1))
+
+            with m.If(self.data.valid):
+                m.d.sync += dclk.o.eq(~dclk.o)
+                with m.If(dclk.o):
+                    m.d.sync += index.eq(index + 1)
+                    with m.If(index == 7):
+                        m.d.comb += self.data.ready.eq(1)
+
+            return m
+
+    def test_output_serializer():
+        dclk_port = io.SimulationPort("o", 1)
+        dout_port = io.SimulationPort("o", 1)
+
+        dut = OutputSerializer(dclk_port, dout_port)
+
+        async def testbench_write_data(ctx):
+            ctx.set(dut.data.payload, 0xA1)
+            ctx.set(dut.data.valid, 1)
+            await ctx.tick().until(dut.data.ready)
+            ctx.set(dut.data.valid, 0)
+
+        async def testbench_sample_output(ctx):
+            for bit in [1,0,0,0,0,1,0,1]:
+                _, dout_value = await ctx.posedge(dut.dclk_port.o).sample(dut.dout_port.o)
+                assert ctx.get(dut.dout_port.oe) == 1, "DUT is not driving the data output"
+                assert dout_value == bit, "DUT drives the wrong value on data output"
+
+        sim = Simulator(dut)
+        sim.add_clock(1e-6)
+        sim.add_testbench(testbench_write_data)
+        sim.add_testbench(testbench_sample_output)
+        sim.run()
+
+.. testcode::
+    :hide:
+
+    test_output_serializer()
+
+
 Ports
 -----
 
@@ -199,6 +268,7 @@ Ports
 .. autoclass:: PortLike
 .. autoclass:: SingleEndedPort
 .. autoclass:: DifferentialPort
+.. autoclass:: SimulationPort
 
 
 Buffers
diff --git a/pyproject.toml b/pyproject.toml
index 1d74683c6..37abedab1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -75,13 +75,14 @@ test = [
 ]
 docs = [
   "sphinx~=7.1",
-  "sphinxcontrib-platformpicker~=1.3",
-  "sphinxcontrib-yowasp-wavedrom==1.7", # exact version to avoid changes in rendering
+  "sphinxcontrib-platformpicker~=1.4",
+  "sphinxcontrib-yowasp-wavedrom==1.8", # exact version to avoid changes in rendering
   "sphinx-rtd-theme~=2.0",
   "sphinx-autobuild",
 ]
 examples = [
-  "amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git"
+  # pin to last commit with py3.8 support
+  "amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git@19b97324ecf9111c5d16377af79f82aad761c476"
 ]
 
 [tool.pdm.scripts]
diff --git a/tests/test_back_rtlil.py b/tests/test_back_rtlil.py
index 6c4dbcc81..eefc39911 100644
--- a/tests/test_back_rtlil.py
+++ b/tests/test_back_rtlil.py
@@ -1214,7 +1214,6 @@ def test_sync(self):
             wire width 8 output 3 \o
             wire width 8 $1
             process $2
-                assign $1 [7:0] \o [7:0]
                 assign $1 [7:0] \i [7:0]
                 switch \rst [0]
                     case 1'1
@@ -2009,6 +2008,51 @@ def test_print_align(self):
         end
         """)
 
+    def test_escape_curly(self):
+        m = Module()
+        m.d.comb += [
+            Print("{"),
+            Print("}"),
+        ]
+        self.assertRTLIL(m, [], R"""
+        attribute \generator "Amaranth"
+        attribute \top 1
+        module \top
+            wire width 1 $1
+            wire width 1 $2
+            process $3
+                assign $1 [0] 1'0
+                assign $1 [0] 1'1
+            end
+            cell $print $4
+                parameter \FORMAT "{{\n"
+                parameter \ARGS_WIDTH 0
+                parameter signed \PRIORITY 32'11111111111111111111111111111110
+                parameter \TRG_ENABLE 0
+                parameter \TRG_WIDTH 0
+                parameter \TRG_POLARITY 0
+                connect \EN $1 [0]
+                connect \ARGS { }
+                connect \TRG { }
+            end
+            process $5
+                assign $2 [0] 1'0
+                assign $2 [0] 1'1
+            end
+            cell $print $6
+                parameter \FORMAT "}}\n"
+                parameter \ARGS_WIDTH 0
+                parameter signed \PRIORITY 32'11111111111111111111111111111100
+                parameter \TRG_ENABLE 0
+                parameter \TRG_WIDTH 0
+                parameter \TRG_POLARITY 0
+                connect \EN $2 [0]
+                connect \ARGS { }
+                connect \TRG { }
+            end
+        end
+        """)
+
 
 class DetailTestCase(RTLILTestCase):
     def test_enum(self):
@@ -2024,7 +2068,7 @@ class MyEnum(enum.Enum, shape=unsigned(2)):
         attribute \generator "Amaranth"
         attribute \top 1
         module \top
-            attribute \enum_base_type "MyEnum"
+            attribute \enum_base_type "DetailTestCase.test_enum.<locals>.MyEnum"
             attribute \enum_value_00 "A"
             attribute \enum_value_01 "B"
             attribute \enum_value_10 "C"
@@ -2052,7 +2096,7 @@ class Meow(data.Struct):
         attribute \top 1
         module \top
             wire width 13 input 0 \sig
-            attribute \enum_base_type "MyEnum"
+            attribute \enum_base_type "DetailTestCase.test_struct.<locals>.MyEnum"
             attribute \enum_value_00 "A"
             attribute \enum_value_01 "B"
             attribute \enum_value_10 "C"
diff --git a/tests/test_build_res.py b/tests/test_build_res.py
index bda61b480..758cdce63 100644
--- a/tests/test_build_res.py
+++ b/tests/test_build_res.py
@@ -84,6 +84,12 @@ def test_request_with_dir(self):
         scl_buffer._MustUse__silence = True
         sda_buffer._MustUse__silence = True
 
+    def test_request_subsignal_dash(self):
+        with _ignore_deprecated():
+            i2c = self.cm.request("i2c", 0, dir="-")
+        self.assertIsInstance(i2c.sda, SingleEndedPort)
+        self.assertIsInstance(i2c.scl, SingleEndedPort)
+
     def test_request_tristate(self):
         with _ignore_deprecated():
             i2c = self.cm.request("i2c", 0)
@@ -226,17 +232,17 @@ def test_request_clock(self):
         ) = self.cm.iter_pins()
         clk100_buffer._MustUse__silence = True
         clk50_buffer._MustUse__silence = True
-        self.assertEqual(list(self.cm.iter_clock_constraints()), [
-            (clk100.i, clk100_port.p, 100e6),
-            (clk50.i, clk50_port.io, 50e6)
+        self.assertEqual(list(self.cm.iter_port_clock_constraints()), [
+            (clk100_port.p, 100e6),
+            (clk50_port.io, 50e6)
         ])
 
     def test_add_clock(self):
         with _ignore_deprecated():
             i2c = self.cm.request("i2c")
         self.cm.add_clock_constraint(i2c.scl.o, 100e3)
-        self.assertEqual(list(self.cm.iter_clock_constraints()), [
-            (i2c.scl.o, None, 100e3)
+        self.assertEqual(list(self.cm.iter_signal_clock_constraints()), [
+            (i2c.scl.o, 100e3)
         ])
         ((_, _, scl_buffer), (_, _, sda_buffer)) = self.cm.iter_pins()
         scl_buffer._MustUse__silence = True
@@ -269,7 +275,7 @@ def test_wrong_lookup(self):
 
     def test_wrong_clock_signal(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Object None is not a Signal$"):
+                r"^Object None is not a Signal or IOPort$"):
             self.cm.add_clock_constraint(None, 10e6)
 
     def test_wrong_clock_frequency(self):
@@ -335,10 +341,8 @@ def test_wrong_request_with_xdr_dict(self):
 
     def test_wrong_clock_constraint_twice(self):
         with _ignore_deprecated():
-            clk100 = self.cm.request("clk100")
-        (pin, port, buffer), = self.cm.iter_pins()
-        buffer._MustUse__silence = True
+            clk100 = self.cm.request("clk100", dir="-")
         with self.assertRaisesRegex(ValueError,
-                (r"^Cannot add clock constraint on \(sig clk100_0__i\), which is already "
+                (r"^Cannot add clock constraint on \(io-port clk100_0__p\), which is already "
                     r"constrained to 100000000\.0 Hz$")):
-            self.cm.add_clock_constraint(clk100.i, 1e6)
+            self.cm.add_clock_constraint(clk100.p, 1e6)
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 666968fe6..f4ae0065b 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -185,7 +185,7 @@ def from_bits(self, bits):
 class ShapeCastableTestCase(FHDLTestCase):
     def test_no_override(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockShapeCastableNoOverride' deriving from 'ShapeCastable' must "
+                r"^Class '.+\.MockShapeCastableNoOverride' deriving from 'ShapeCastable' must "
                 r"override the 'as_shape' method$"):
             class MockShapeCastableNoOverride(ShapeCastable):
                 def __init__(self):
@@ -213,7 +213,7 @@ def test_abstract(self):
 
     def test_no_from_bits(self):
         with self.assertWarnsRegex(DeprecationWarning,
-                r"^Class 'MockShapeCastableNoFromBits' deriving from 'ShapeCastable' does "
+                r"^Class '.+\.MockShapeCastableNoFromBits' deriving from 'ShapeCastable' does "
                 r"not override the 'from_bits' method, which will be required in Amaranth 0.6$"):
             class MockShapeCastableNoFromBits(ShapeCastable):
                 def __init__(self, dest):
@@ -521,6 +521,20 @@ def test_hash(self):
         with self.assertRaises(TypeError):
             hash(Const(0))
 
+    def test_enum(self):
+        e1 = Const(UnsignedEnum.FOO)
+        self.assertIsInstance(e1, Const)
+        self.assertEqual(e1.shape(), unsigned(2))
+        e2 = Const(SignedEnum.FOO)
+        self.assertIsInstance(e2, Const)
+        self.assertEqual(e2.shape(), signed(2))
+        e3 = Const(TypedEnum.FOO)
+        self.assertIsInstance(e3, Const)
+        self.assertEqual(e3.shape(), unsigned(2))
+        e4 = Const(UnsignedEnum.FOO, 4)
+        self.assertIsInstance(e4, Const)
+        self.assertEqual(e4.shape(), unsigned(4))
+
     def test_shape_castable(self):
         class MockConstValue(ValueCastable):
             def __init__(self, value):
@@ -528,7 +542,7 @@ def __init__(self, value):
 
             def shape(self):
                 return MockConstShape()
-            
+
             def as_value(self):
                 return Const(self.value, 8)
 
@@ -1250,6 +1264,14 @@ def test_init_wrong_too_wide(self):
                 r"^Initial value -2 will be truncated to the signal shape signed\(1\)$"):
             Signal(signed(1), init=-2)
 
+    def test_init_truncated(self):
+        s1 = Signal(unsigned(2), init=-1)
+        self.assertEqual(s1.init, 0b11)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(action="ignore", category=SyntaxWarning)
+            s2 = Signal(signed(2), init=-33)
+            self.assertEqual(s2.init, -1)
+
     def test_init_wrong_fencepost(self):
         with self.assertRaisesRegex(SyntaxError,
                 r"^Initial value 10 equals the non-inclusive end of the signal shape "
@@ -1327,7 +1349,7 @@ class Color(Enum):
             BLUE = 2
         s = Signal(decoder=Color)
         self.assertEqual(s.decoder, Color)
-        self.assertRepr(s._format, "(format-enum (sig s) 'Color' (1 'RED') (2 'BLUE'))")
+        self.assertRepr(s._format, f"(format-enum (sig s) '{Color.__qualname__}' (1 'RED') (2 'BLUE'))")
 
     def test_enum(self):
         s1 = Signal(UnsignedEnum)
@@ -1515,14 +1537,14 @@ def __getattr__(self, attr):
 class ValueCastableTestCase(FHDLTestCase):
     def test_no_override(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockValueCastableNoOverrideAsValue' deriving from 'ValueCastable' must "
+                r"^Class '.+\.MockValueCastableNoOverrideAsValue' deriving from 'ValueCastable' must "
                 r"override the 'as_value' method$"):
             class MockValueCastableNoOverrideAsValue(ValueCastable):
                 def __init__(self):
                     pass
 
         with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockValueCastableNoOverrideShapec' deriving from 'ValueCastable' must "
+                r"^Class '.+\.MockValueCastableNoOverrideShapec' deriving from 'ValueCastable' must "
                 r"override the 'shape' method$"):
             class MockValueCastableNoOverrideShapec(ValueCastable):
                 def __init__(self):
diff --git a/tests/test_hdl_dsl.py b/tests/test_hdl_dsl.py
index ecbed9d04..0ab6b48b5 100644
--- a/tests/test_hdl_dsl.py
+++ b/tests/test_hdl_dsl.py
@@ -34,7 +34,6 @@ def test_d_comb(self):
         m = Module()
         m.d.comb += self.c1.eq(1)
         m._flush()
-        self.assertEqual(m._driving[self.c1], "comb")
         self.assertRepr(m._statements["comb"], """(
             (eq (sig c1) (const 1'd1))
         )""")
@@ -43,7 +42,6 @@ def test_d_sync(self):
         m = Module()
         m.d.sync += self.c1.eq(1)
         m._flush()
-        self.assertEqual(m._driving[self.c1], "sync")
         self.assertRepr(m._statements["sync"], """(
             (eq (sig c1) (const 1'd1))
         )""")
@@ -52,7 +50,6 @@ def test_d_pix(self):
         m = Module()
         m.d.pix += self.c1.eq(1)
         m._flush()
-        self.assertEqual(m._driving[self.c1], "pix")
         self.assertRepr(m._statements["pix"], """(
             (eq (sig c1) (const 1'd1))
         )""")
@@ -61,7 +58,6 @@ def test_d_index(self):
         m = Module()
         m.d["pix"] += self.c1.eq(1)
         m._flush()
-        self.assertEqual(m._driving[self.c1], "pix")
         self.assertRepr(m._statements["pix"], """(
             (eq (sig c1) (const 1'd1))
         )""")
@@ -74,7 +70,7 @@ def test_d_no_conflict(self):
     def test_d_conflict(self):
         m = Module()
         with self.assertRaisesRegex(SyntaxError,
-                (r"^Driver-driver conflict: trying to drive \(sig c1\) from d\.sync, but it "
+                (r"^Driver-driver conflict: trying to drive \(sig c1\) bit 0 from d\.sync, but it "
                     r"is already driven from d\.comb$")):
             m.d.comb += self.c1.eq(1)
             m.d.sync += self.c1.eq(1)
diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py
index 5b6a682cd..4899d188b 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -265,7 +265,7 @@ def test_port_domain(self):
             (cell 1 0 (+ (cat 5.0:4 1'd0) 5'd1))
             (cell 2 0 (matches 0.3 1))
             (cell 3 0 (priority_match 1 2.0))
-            (cell 4 0 (assignment_list 5.0:4 (1 0:4 1.0:4) (3.0 0:4 4'd0)))
+            (cell 4 0 (assignment_list 1.0:4 (3.0 0:4 4'd0)))
             (cell 5 0 (flipflop 4.0:4 0 pos 0.2 0))
         )
         """)
@@ -282,7 +282,7 @@ def test_port_autodomain(self):
             (cell 1 0 (+ (cat 5.0:4 1'd0) 5'd1))
             (cell 2 0 (matches 0.3 1))
             (cell 3 0 (priority_match 1 2.0))
-            (cell 4 0 (assignment_list 5.0:4 (1 0:4 1.0:4) (3.0 0:4 4'd0)))
+            (cell 4 0 (assignment_list 1.0:4 (3.0 0:4 4'd0)))
             (cell 5 0 (flipflop 4.0:4 0 pos 0.2 0))
         )
         """)
@@ -3411,6 +3411,121 @@ def test_assert(self):
         """)
 
 
+class SplitDriverTestCase(FHDLTestCase):
+    def test_split_domain(self):
+        m = Module()
+        o = Signal(10, init=0x123)
+        i1 = Signal(2)
+        i2 = Signal(2)
+        i3 = Signal(2)
+        i4 = Signal(2)
+        cond = Signal()
+        m.domains.a = ClockDomain()
+        m.domains.b = ClockDomain()
+        m.d.a += o[:2].eq(i1)
+        m.d.b += o[2:4].eq(i2)
+        with m.If(cond):
+            m.d.a += o[4:6].eq(i3)
+            m.d.comb += o[8:10].eq(i4)
+        nl = build_netlist(Fragment.get(m, None), [
+            o, i1, i2, i3, i4, cond,
+            ClockSignal("a"), ResetSignal("a"),
+            ClockSignal("b"), ResetSignal("b"),
+        ])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'i1' 0.2:4)
+                (input 'i2' 0.4:6)
+                (input 'i3' 0.6:8)
+                (input 'i4' 0.8:10)
+                (input 'cond' 0.10)
+                (input 'a_clk' 0.11)
+                (input 'a_rst' 0.12)
+                (input 'b_clk' 0.13)
+                (input 'b_rst' 0.14)
+                (output 'o' (cat 6.0:2 12.0:2 8.0:2 2'd0 13.0:2))
+            )
+            (cell 0 0 (top
+                (input 'i1' 2:4)
+                (input 'i2' 4:6)
+                (input 'i3' 6:8)
+                (input 'i4' 8:10)
+                (input 'cond' 10:11)
+                (input 'a_clk' 11:12)
+                (input 'a_rst' 12:13)
+                (input 'b_clk' 13:14)
+                (input 'b_rst' 14:15)
+                (output 'o' (cat 6.0:2 12.0:2 8.0:2 2'd0 13.0:2))
+            ))
+            (cell 1 0 (matches 0.10 1))
+            (cell 2 0 (priority_match 1 1.0))
+            (cell 3 0 (matches 0.12 1))
+            (cell 4 0 (priority_match 1 3.0))
+            (cell 5 0 (assignment_list 0.2:4 (4.0 0:2 2'd3)))
+            (cell 6 0 (flipflop 5.0:2 3 pos 0.11 0))
+            (cell 7 0 (assignment_list 8.0:2 (2.0 0:2 0.6:8) (4.0 0:2 2'd2)))
+            (cell 8 0 (flipflop 7.0:2 2 pos 0.11 0))
+            (cell 9 0 (matches 0.14 1))
+            (cell 10 0 (priority_match 1 9.0))
+            (cell 11 0 (assignment_list 0.4:6 (10.0 0:2 2'd0)))
+            (cell 12 0 (flipflop 11.0:2 0 pos 0.13 0))
+            (cell 13 0 (assignment_list 2'd1 (2.0 0:2 0.8:10)))
+        )
+        """)
+
+    def test_split_module(self):
+        m = Module()
+        m.submodules.m1 = m1 = Module()
+        m.submodules.m2 = m2 = Module()
+
+        i1 = Signal(4)
+        i2 = Signal(4)
+        i3 = Signal(2)
+        cond = Signal()
+        o = Signal(8)
+        m1.d.comb += o[:4].eq(i1)
+        m2.d.comb += o[4:].eq(i2)
+        with m2.If(cond):
+            m2.d.comb += o[5:7].eq(i3)
+
+        nl = build_netlist(Fragment.get(m, None), [
+            o, i1, i2, i3, cond,
+        ])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'i1' 0.2:6)
+                (input 'i2' 0.6:10)
+                (input 'i3' 0.10:12)
+                (input 'cond' 0.12)
+                (output 'o' (cat 0.2:6 3.0:4))
+            )
+            (module 1 0 ('top' 'm1')
+                (input 'i1' 0.2:6)
+                (input 'port$3$0' 3.0:4)
+            )
+            (module 2 0 ('top' 'm2')
+                (input 'port$0$2' 0.2:6)
+                (input 'i2' 0.6:10)
+                (input 'i3' 0.10:12)
+                (input 'cond' 0.12)
+                (output 'port$3$0' 3.0:4)
+            )
+            (cell 0 0 (top
+                (input 'i1' 2:6)
+                (input 'i2' 6:10)
+                (input 'i3' 10:12)
+                (input 'cond' 12:13)
+                (output 'o' (cat 0.2:6 3.0:4))
+            ))
+            (cell 1 2 (matches 0.12 1))
+            (cell 2 2 (priority_match 1 1.0))
+            (cell 3 2 (assignment_list 0.6:10 (2.0 1:3 0.10:12)))
+        )
+        """)
+
+
 class ConflictTestCase(FHDLTestCase):
     def test_domain_conflict(self):
         s = Signal()
@@ -3420,7 +3535,7 @@ def test_domain_conflict(self):
         m1.d.comb += s.eq(2)
         m.submodules.m1 = m1
         with self.assertRaisesRegex(DriverConflict,
-                r"^Signal \(sig s\) driven from domain comb at "
+                r"^Signal \(sig s\) bit 0 driven from domain comb at "
                 r".*test_hdl_ir.py:\d+ and domain sync at "
                 r".*test_hdl_ir.py:\d+$"):
             build_netlist(Fragment.get(m, None), [])
@@ -3433,7 +3548,7 @@ def test_module_conflict(self):
         m1.d.sync += s.eq(2)
         m.submodules.m1 = m1
         with self.assertRaisesRegex(DriverConflict,
-                r"^Signal \(sig s\) driven from module top\.m1 at "
+                r"^Signal \(sig s\) bit 0 driven from module top\.m1 at "
                 r".*test_hdl_ir.py:\d+ and module top at "
                 r".*test_hdl_ir.py:\d+$"):
             build_netlist(Fragment.get(m, None), [])
@@ -3523,7 +3638,7 @@ class MyEnum(enum.Enum, shape=unsigned(2)):
         ])
         self.assertEqual(nl.signal_fields[s1.as_value()], {
             (): SignalField(nl.signals[s1.as_value()], signed=False),
-            ('a',): SignalField(nl.signals[s1.as_value()][0:2], signed=False, enum_name="MyEnum", enum_variants={
+            ('a',): SignalField(nl.signals[s1.as_value()][0:2], signed=False, enum_name=MyEnum.__qualname__, enum_variants={
                 0: "A",
                 1: "B",
                 2: "C",
@@ -3531,7 +3646,7 @@ class MyEnum(enum.Enum, shape=unsigned(2)):
             ('b',): SignalField(nl.signals[s1.as_value()][2:5], signed=True)
         })
         self.assertEqual(nl.signal_fields[s2.as_value()], {
-            (): SignalField(nl.signals[s2.as_value()], signed=False, enum_name="MyEnum", enum_variants={
+            (): SignalField(nl.signals[s2.as_value()], signed=False, enum_name=MyEnum.__qualname__, enum_variants={
                 0: "A",
                 1: "B",
                 2: "C",
@@ -3563,6 +3678,30 @@ def test_cycle(self):
                 r"$"):
             build_netlist(Fragment.get(m, None), [])
 
+    def test_assignment_cycle(self):
+        a = Signal(2)
+        m = Module()
+
+        with m.If(a[0]):
+            m.d.comb += a[0].eq(1)
+
+        with self.assertRaisesRegex(CombinationalCycle,
+                r"^Combinational cycle detected, path:\n"
+                r".*test_hdl_ir.py:\d+: cell Matches bit 0\n"
+                r".*test_hdl_ir.py:\d+: signal a bit 0\n"
+                r".*test_hdl_ir.py:\d+: cell AssignmentList bit 0\n"
+                r".*test_hdl_ir.py:\d+: cell PriorityMatch bit 0\n"
+                r"$"):
+            build_netlist(Fragment.get(m, None), [])
+
+        m = Module()
+
+        with m.If(a[0]):
+            m.d.comb += a[1].eq(1)
+
+        # no cycle here, a[1] gets assigned and a[0] gets checked
+        build_netlist(Fragment.get(m, None), [])
+
 
 class DomainLookupTestCase(FHDLTestCase):
     def test_domain_lookup(self):
@@ -3622,4 +3761,4 @@ def test_require_renamed(self):
         m.submodules += DomainRenamer("test")(RequirePosedge("sync"))
         with self.assertRaisesRegex(DomainRequirementFailed,
                 r"^Domain test has a negedge clock, but posedge clock is required by top.U\$0 at .*$"):
-            Fragment.get(m, None).prepare()
\ No newline at end of file
+            Fragment.get(m, None).prepare()
diff --git a/tests/test_hdl_mem.py b/tests/test_hdl_mem.py
index f165622de..722b52fd4 100644
--- a/tests/test_hdl_mem.py
+++ b/tests/test_hdl_mem.py
@@ -33,6 +33,33 @@ def test_row_elab(self):
                 r"^Value \(memory-row \(memory-data data\) 0\) can only be used in simulator processes$"):
             m.d.comb += data[0].eq(1)
 
+
+class InitTestCase(FHDLTestCase):
+    def test_ones(self):
+        init = MemoryData.Init([-1, 12], shape=8, depth=2)
+        self.assertEqual(list(init), [0xff, 12])
+        init = MemoryData.Init([-1, -12], shape=signed(8), depth=2)
+        self.assertEqual(list(init), [-1, -12])
+
+    def test_trunc(self):
+        with self.assertWarnsRegex(SyntaxWarning,
+                r"^Initial value -2 is signed, but the memory shape is unsigned\(8\)$"):
+            init = MemoryData.Init([-2, 12], shape=8, depth=2)
+        self.assertEqual(list(init), [0xfe, 12])
+        with self.assertWarnsRegex(SyntaxWarning,
+                r"^Initial value 258 will be truncated to the memory shape unsigned\(8\)$"):
+            init = MemoryData.Init([258, 129], shape=8, depth=2)
+        self.assertEqual(list(init), [2, 129])
+        with self.assertWarnsRegex(SyntaxWarning,
+                r"^Initial value 128 will be truncated to the memory shape signed\(8\)$"):
+            init = MemoryData.Init([128], shape=signed(8), depth=1)
+        self.assertEqual(list(init), [-128])
+        with self.assertWarnsRegex(SyntaxWarning,
+                r"^Initial value -129 will be truncated to the memory shape signed\(8\)$"):
+            init = MemoryData.Init([-129], shape=signed(8), depth=1)
+        self.assertEqual(list(init), [127])
+
+
 class MemoryTestCase(FHDLTestCase):
     def test_name(self):
         with _ignore_deprecated():
@@ -73,7 +100,7 @@ def test_init_wrong_type(self):
         with _ignore_deprecated():
             with self.assertRaisesRegex(TypeError,
                     (r"^Memory initialization value at address 1: "
-                        r"'str' object cannot be interpreted as an integer$")):
+                        r"Initial value must be a constant-castable expression, not '0'$")):
                 m = Memory(width=8, depth=4, init=[1, "0"])
 
     def test_attrs(self):
diff --git a/tests/test_hdl_xfrm.py b/tests/test_hdl_xfrm.py
index 0ab2eeeaa..389629655 100644
--- a/tests/test_hdl_xfrm.py
+++ b/tests/test_hdl_xfrm.py
@@ -227,6 +227,7 @@ def setUp(self):
         self.s1 = Signal()
         self.s2 = Signal(init=1)
         self.s3 = Signal(init=1, reset_less=True)
+        self.s4 = Signal(8, init=0x3a)
         self.c1 = Signal()
 
     def test_reset_default(self):
@@ -281,6 +282,40 @@ def test_reset_value(self):
         )
         """)
 
+    def test_reset_mask(self):
+        f = Fragment()
+        f.add_statements("sync", self.s4[2:4].eq(0))
+
+        f = ResetInserter(self.c1)(f)
+        self.assertRepr(f.statements["sync"], """
+        (
+            (eq (slice (sig s4) 2:4) (const 1'd0))
+            (switch (sig c1)
+                (case 1 (eq (slice (sig s4) 2:4) (slice (const 8'd58) 2:4)))
+            )
+        )
+        """)
+
+        f = Fragment()
+        f.add_statements("sync", self.s4[2:4].eq(0))
+        f.add_statements("sync", self.s4[3:5].eq(0))
+        f.add_statements("sync", self.s4[6:10].eq(0))
+
+        f = ResetInserter(self.c1)(f)
+        self.assertRepr(f.statements["sync"], """
+        (
+            (eq (slice (sig s4) 2:4) (const 1'd0))
+            (eq (slice (sig s4) 3:5) (const 1'd0))
+            (eq (slice (sig s4) 6:8) (const 1'd0))
+            (switch (sig c1)
+                (case 1 
+                    (eq (slice (sig s4) 2:5) (slice (const 8'd58) 2:5))
+                    (eq (slice (sig s4) 6:8) (slice (const 8'd58) 6:8))
+                )
+            )
+        )
+        """)
+
     def test_reset_less(self):
         f = Fragment()
         f.add_statements("sync", self.s3.eq(0))
@@ -423,3 +458,31 @@ def test_composition(self):
             )
         )
         """)
+
+class LHSMaskCollectorTestCase(FHDLTestCase):
+    def test_slice(self):
+        s = Signal(8)
+        lhs = LHSMaskCollector()
+        lhs.visit_value(s[2:5], ~0)
+        self.assertEqual(lhs.lhs[s], 0x1c)
+
+    def test_slice_slice(self):
+        s = Signal(8)
+        lhs = LHSMaskCollector()
+        lhs.visit_value(s[2:7][1:3], ~0)
+        self.assertEqual(lhs.lhs[s], 0x18)
+
+    def test_slice_concat(self):
+        s1 = Signal(8)
+        s2 = Signal(8)
+        lhs = LHSMaskCollector()
+        lhs.visit_value(Cat(s1, s2)[4:11], ~0)
+        self.assertEqual(lhs.lhs[s1], 0xf0)
+        self.assertEqual(lhs.lhs[s2], 0x07)
+
+    def test_slice_part(self):
+        s = Signal(8)
+        idx = Signal(8)
+        lhs = LHSMaskCollector()
+        lhs.visit_value(s.bit_select(idx, 5)[1:3], ~0)
+        self.assertEqual(lhs.lhs[s], 0xff)
diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py
index 697f36650..bf2583eeb 100644
--- a/tests/test_lib_data.py
+++ b/tests/test_lib_data.py
@@ -477,6 +477,10 @@ def test_const_wrong(self):
                 r"^Layout constant initializer must be a mapping or a sequence, not "
                 r"<.+?object.+?>$"):
             sl.const(object())
+        with self.assertRaisesRegex(ValueError,
+                r"^Layout constant initializer refers to key 'g', which is not a part "
+                r"of the layout$"):
+            sl.const({"g": 1})
         sl2 = data.StructLayout({"f": unsigned(2)})
         with self.assertRaisesRegex(ValueError,
                 r"^Const layout StructLayout.* differs from shape layout StructLayout.*$"):
@@ -610,10 +614,52 @@ def test_getitem(self):
         self.assertEqual(v["q"].shape(), signed(1))
         self.assertRepr(v["r"][0], "(slice (slice (sig v) 0:4) 0:2)")
         self.assertRepr(v["r"][1], "(slice (slice (sig v) 0:4) 2:4)")
+        self.assertRepr(v["r"][-2], "(slice (slice (sig v) 0:4) 0:2)")
+        self.assertRepr(v["r"][-1], "(slice (slice (sig v) 0:4) 2:4)")
         self.assertRepr(v["r"][i], "(part (slice (sig v) 0:4) (sig i) 2 2)")
         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)")
 
+    def test_getitem_slice(self):
+        a = Signal(data.ArrayLayout(unsigned(2), 5))
+        self.assertEqual(a[1:3].shape(), data.ArrayLayout(unsigned(2), 2))
+        self.assertRepr(a[1:3].as_value(), "(slice (sig a) 2:6)")
+        self.assertRepr(a[2:].as_value(), "(slice (sig a) 4:10)")
+        self.assertRepr(a[:-2].as_value(), "(slice (sig a) 0:6)")
+        self.assertRepr(a[-1:].as_value(), "(slice (sig a) 8:10)")
+        self.assertRepr(a[::-1].as_value(), """
+            (cat
+                (slice (sig a) 8:10)
+                (slice (sig a) 6:8)
+                (slice (sig a) 4:6)
+                (slice (sig a) 2:4)
+                (slice (sig a) 0:2)
+            )
+        """)
+        self.assertRepr(a[::2].as_value(), """
+            (cat
+                (slice (sig a) 0:2)
+                (slice (sig a) 4:6)
+                (slice (sig a) 8:10)
+            )
+        """)
+        self.assertRepr(a[1::2].as_value(), """
+            (cat
+                (slice (sig a) 2:4)
+                (slice (sig a) 6:8)
+            )
+        """)
+
+    def test_array_iter(self):
+        a = Signal(data.ArrayLayout(unsigned(2), 5))
+        l = list(a)
+        self.assertEqual(len(l), 5)
+        self.assertRepr(l[0], "(slice (sig a) 0:2)")
+        self.assertRepr(l[1], "(slice (sig a) 2:4)")
+        self.assertRepr(l[2], "(slice (sig a) 4:6)")
+        self.assertRepr(l[3], "(slice (sig a) 6:8)")
+        self.assertRepr(l[4], "(slice (sig a) 8:10)")
+
     def test_getitem_custom_call(self):
         class Reverser(ShapeCastable):
             def as_shape(self):
@@ -670,9 +716,17 @@ def test_index_wrong_struct_dynamic(self):
                 r"with a value$"):
             Signal(data.StructLayout({}))[Signal(1)]
 
+    def test_index_wrong_oob(self):
+        with self.assertRaisesRegex(IndexError,
+                r"^Index 2 is out of range for array layout of length 2$"):
+            Signal(data.ArrayLayout(unsigned(2), 2))[2]
+        with self.assertRaisesRegex(IndexError,
+                r"^Index -3 is out of range for array layout of length 2$"):
+            Signal(data.ArrayLayout(unsigned(2), 2))[-3]
+
     def test_index_wrong_slice(self):
         with self.assertRaisesRegex(TypeError,
-                r"^View cannot be indexed with a slice; did you mean to call `.as_value\(\)` "
+                r"^Non-array view cannot be indexed with a slice; did you mean to call `.as_value\(\)` "
                 r"first\?$"):
             Signal(data.StructLayout({}))[0:1]
 
@@ -740,7 +794,7 @@ def test_bug_837_array_layout_getattr(self):
                 r"^View with an array layout does not have fields$"):
             Signal(data.ArrayLayout(unsigned(1), 1), init=[0]).init
 
-    def test_eq(self):
+    def test_compare(self):
         s1 = Signal(data.StructLayout({"a": unsigned(2)}))
         s2 = Signal(data.StructLayout({"a": unsigned(2)}))
         s3 = Signal(data.StructLayout({"a": unsigned(1), "b": unsigned(1)}))
@@ -763,6 +817,14 @@ def test_eq(self):
                 r"with the same layout, not .*$"):
             s1 != Const(0, 2)
 
+    def test_len(self):
+        s1 = Signal(data.StructLayout({"a": unsigned(2)}))
+        with self.assertRaisesRegex(TypeError,
+                r"^`len\(\)` can only be used on views of array layout, not StructLayout.*$"):
+            len(s1)
+        s2 = Signal(data.ArrayLayout(2, 3))
+        self.assertEqual(len(s2), 3)
+
     def test_operator(self):
         s1 = Signal(data.StructLayout({"a": unsigned(2)}))
         s2 = Signal(unsigned(2))
@@ -877,12 +939,28 @@ def test_getitem(self):
         self.assertEqual(v["q"], -1)
         self.assertEqual(v["r"][0], 3)
         self.assertEqual(v["r"][1], 2)
+        self.assertEqual(v["r"][-2], 3)
+        self.assertEqual(v["r"][-1], 2)
         self.assertRepr(v["r"][i], "(part (const 4'd11) (sig i) 2 2)")
         self.assertEqual(v["t"][0], data.Const(l, 2))
         self.assertEqual(v["t"][1], data.Const(l, 2))
         self.assertEqual(v["t"][0]["u"], 0)
         self.assertEqual(v["t"][1]["v"], 1)
 
+    def test_getitem_slice(self):
+        def A(n):
+            return data.ArrayLayout(unsigned(4), n)
+        v = data.Const(data.ArrayLayout(unsigned(4), 5), 0xabcde)
+        self.assertEqual(v[1:3], data.Const(A(2), 0xcd))
+        self.assertEqual(v[2:], data.Const(A(3), 0xabc))
+        self.assertEqual(v[:-2], data.Const(A(3), 0xcde))
+        self.assertEqual(v[-1:], data.Const(A(1), 0xa))
+        self.assertEqual(v[::-1], data.Const(A(5), 0xedcba))
+
+    def test_array_iter(self):
+        v = data.Const(data.ArrayLayout(unsigned(4), 5), 0xabcde)
+        self.assertEqual(list(v), [0xe, 0xd, 0xc, 0xb, 0xa])
+
     def test_getitem_custom_call(self):
         class Reverser(ShapeCastable):
             def as_shape(self):
@@ -961,11 +1039,12 @@ def test_bug_837_array_layout_getattr(self):
                 r"^Constant with an array layout does not have fields$"):
             data.Const(data.ArrayLayout(unsigned(1), 1), 0).init
 
-    def test_eq(self):
+    def test_compare(self):
         c1 = data.Const(data.StructLayout({"a": unsigned(2)}), 1)
         c2 = data.Const(data.StructLayout({"a": unsigned(2)}), 1)
         c3 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
         c4 = data.Const(data.StructLayout({"a": unsigned(1), "b": unsigned(1)}), 2)
+        c5 = data.Const(data.ArrayLayout(2, 4), 0b11100100)
         s1 = Signal(data.StructLayout({"a": unsigned(2)}))
         self.assertTrue(c1 == c2)
         self.assertFalse(c1 != c2)
@@ -975,13 +1054,23 @@ def test_eq(self):
         self.assertRepr(c1 != s1, "(!= (const 2'd1) (sig s1))")
         self.assertRepr(s1 == c1, "(== (sig s1) (const 2'd1))")
         self.assertRepr(s1 != c1, "(!= (sig s1) (const 2'd1))")
+        self.assertTrue(c1 == {"a": 1})
+        self.assertFalse(c1 == {"a": 2})
+        self.assertFalse(c1 != {"a": 1})
+        self.assertTrue(c1 != {"a": 2})
+        self.assertTrue(c5 == [0,1,2,3])
+        self.assertFalse(c5 == [0,1,3,3])
+        self.assertFalse(c5 != [0,1,2,3])
+        self.assertTrue(c5 != [0,1,3,3])
         with self.assertRaisesRegex(TypeError,
-                r"^Constant with layout .* can only be compared to another view or constant with "
-                r"the same layout, not .*$"):
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
             c1 == c4
         with self.assertRaisesRegex(TypeError,
-                r"^Constant with layout .* can only be compared to another view or constant with "
-                r"the same layout, not .*$"):
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
             c1 != c4
         with self.assertRaisesRegex(TypeError,
                 r"^View with layout .* can only be compared to another view or constant with "
@@ -992,21 +1081,53 @@ def test_eq(self):
                 r"the same layout, not .*$"):
             s1 != c4
         with self.assertRaisesRegex(TypeError,
-                r"^Constant with layout .* can only be compared to another view or constant with "
-                r"the same layout, not .*$"):
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
             c4 == s1
         with self.assertRaisesRegex(TypeError,
-                r"^Constant with layout .* can only be compared to another view or constant with "
-                r"the same layout, not .*$"):
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
             c4 != s1
         with self.assertRaisesRegex(TypeError,
-                r"^Constant with layout .* can only be compared to another view or constant with "
-                r"the same layout, not .*$"):
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
             c1 == Const(0, 2)
         with self.assertRaisesRegex(TypeError,
-                r"^Constant with layout .* can only be compared to another view or constant with "
-                r"the same layout, not .*$"):
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
             c1 != Const(0, 2)
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
+            c1 == {"b": 1}
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
+            c1 != {"b": 1}
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
+            c5 == [0,1,2,3,4]
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view, a constant "
+                r"with the same layout, or a dictionary or a list that can be converted to "
+                r"a constant with the same layout, not .*$"):
+            c5 != [0,1,2,3,4]
+
+    def test_len(self):
+        c1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
+        with self.assertRaisesRegex(TypeError,
+                r"^`len\(\)` can only be used on constants of array layout, not StructLayout.*$"):
+            len(c1)
+        c2 = data.Const(data.ArrayLayout(2, 3), 0x12)
+        self.assertEqual(len(c2), 3)
 
     def test_operator(self):
         s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
diff --git a/tests/test_lib_enum.py b/tests/test_lib_enum.py
index ce0c67d30..1cfe1e231 100644
--- a/tests/test_lib_enum.py
+++ b/tests/test_lib_enum.py
@@ -107,9 +107,9 @@ def test_const_shape(self):
         class EnumA(Enum, shape=8):
             Z = 0
             A = 10
-        self.assertRepr(EnumA.const(None), "EnumView(EnumA, (const 8'd0))")
-        self.assertRepr(EnumA.const(10), "EnumView(EnumA, (const 8'd10))")
-        self.assertRepr(EnumA.const(EnumA.A), "EnumView(EnumA, (const 8'd10))")
+        self.assertRepr(EnumA.const(None), f"EnumView({EnumA.__qualname__}, (const 8'd0))")
+        self.assertRepr(EnumA.const(10), f"EnumView({EnumA.__qualname__}, (const 8'd10))")
+        self.assertRepr(EnumA.const(EnumA.A), f"EnumView({EnumA.__qualname__}, (const 8'd10))")
 
     def test_from_bits(self):
         class EnumA(Enum, shape=2):
@@ -138,7 +138,7 @@ class EnumA(IntEnum, shape=signed(4)):
             B = -3
         a = Signal(EnumA)
         self.assertRepr(a, "(sig a)")
-        self.assertRepr(a._format, "(format-enum (sig a) 'EnumA' (0 'A') (-3 'B'))")
+        self.assertRepr(a._format, f"(format-enum (sig a) '{EnumA.__qualname__}' (0 'A') (-3 'B'))")
 
     def test_enum_view(self):
         class EnumA(Enum, shape=signed(4)):
@@ -153,7 +153,7 @@ class EnumB(Enum, shape=signed(4)):
         d = Signal(4)
         self.assertIsInstance(a, EnumView)
         self.assertIs(a.shape(), EnumA)
-        self.assertRepr(a, "EnumView(EnumA, (sig a))")
+        self.assertRepr(a, f"EnumView({EnumA.__qualname__}, (sig a))")
         self.assertRepr(a.as_value(), "(sig a)")
         self.assertRepr(a.eq(c), "(eq (sig a) (sig c))")
         for op in [
@@ -214,7 +214,7 @@ class FlagB(Flag, shape=unsigned(4)):
         c = Signal(FlagA)
         d = Signal(4)
         self.assertIsInstance(a, FlagView)
-        self.assertRepr(a, "FlagView(FlagA, (sig a))")
+        self.assertRepr(a, f"FlagView({FlagA.__qualname__}, (sig a))")
         for op in [
             operator.__add__,
             operator.__sub__,
@@ -260,17 +260,17 @@ class FlagB(Flag, shape=unsigned(4)):
         self.assertRepr(a == FlagA.B, "(== (sig a) (const 4'd4))")
         self.assertRepr(FlagA.B == a, "(== (sig a) (const 4'd4))")
         self.assertRepr(a != FlagA.B, "(!= (sig a) (const 4'd4))")
-        self.assertRepr(a | c, "FlagView(FlagA, (| (sig a) (sig c)))")
-        self.assertRepr(a & c, "FlagView(FlagA, (& (sig a) (sig c)))")
-        self.assertRepr(a ^ c, "FlagView(FlagA, (^ (sig a) (sig c)))")
-        self.assertRepr(~a, "FlagView(FlagA, (& (~ (sig a)) (const 3'd5)))")
-        self.assertRepr(a | FlagA.B, "FlagView(FlagA, (| (sig a) (const 4'd4)))")
+        self.assertRepr(a | c, f"FlagView({FlagA.__qualname__}, (| (sig a) (sig c)))")
+        self.assertRepr(a & c, f"FlagView({FlagA.__qualname__}, (& (sig a) (sig c)))")
+        self.assertRepr(a ^ c, f"FlagView({FlagA.__qualname__}, (^ (sig a) (sig c)))")
+        self.assertRepr(~a, f"FlagView({FlagA.__qualname__}, (& (~ (sig a)) (const 3'd5)))")
+        self.assertRepr(a | FlagA.B, f"FlagView({FlagA.__qualname__}, (| (sig a) (const 4'd4)))")
         if sys.version_info >= (3, 11):
             class FlagC(Flag, shape=unsigned(4), boundary=py_enum.KEEP):
                 A = 1
                 B = 4
             e = Signal(FlagC)
-            self.assertRepr(~e, "FlagView(FlagC, (~ (sig e)))")
+            self.assertRepr(~e, f"FlagView({FlagC.__qualname__}, (~ (sig e)))")
 
     def test_enum_view_wrong(self):
         class EnumA(Enum, shape=signed(4)):
@@ -327,6 +327,4 @@ class EnumA(Enum, shape=unsigned(2)):
             B = 1
 
         sig = Signal(EnumA)
-        self.assertRepr(EnumA.format(sig, ""), """
-        (format-enum (sig sig) 'EnumA' (0 'A') (1 'B'))
-        """)
+        self.assertRepr(EnumA.format(sig, ""), f"(format-enum (sig sig) '{EnumA.__qualname__}' (0 'A') (1 'B'))")
diff --git a/tests/test_lib_io.py b/tests/test_lib_io.py
index 9dc208ede..e904e7f41 100644
--- a/tests/test_lib_io.py
+++ b/tests/test_lib_io.py
@@ -30,6 +30,14 @@ def test_and(self):
             Direction.Bidir & 3
 
 
+class PortLikeTestCase(FHDLTestCase):
+    def test_warn___add__(self):
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"WrongPortLike must override the `__add__` method$"):
+            class WrongPortLike(PortLike):
+                pass
+
+
 class SingleEndedPortTestCase(FHDLTestCase):
     def test_construct(self):
         io = IOPort(4)
@@ -161,6 +169,123 @@ def test_invert(self):
         self.assertRepr(iport, "DifferentialPort((io-port iop), (io-port ion), invert=(False, True, False, True), direction=Direction.Output)")
 
 
+class SimulationPortTestCase(FHDLTestCase):
+    def test_construct(self):
+        port_io = SimulationPort("io", 2)
+        self.assertEqual(port_io.direction, Direction.Bidir)
+        self.assertEqual(len(port_io), 2)
+        self.assertEqual(port_io.invert, (False, False))
+        self.assertIsInstance(port_io.i, Signal)
+        self.assertEqual(port_io.i.shape(), unsigned(2))
+        self.assertIsInstance(port_io.o, Signal)
+        self.assertEqual(port_io.o.shape(), unsigned(2))
+        self.assertEqual(port_io.o.init, 0)
+        self.assertIsInstance(port_io.oe, Signal)
+        self.assertEqual(port_io.oe.shape(), unsigned(2))
+        self.assertEqual(port_io.oe.init, 0)
+        self.assertRepr(port_io, "SimulationPort(i=(sig port_io__i), o=(sig port_io__o), oe=(sig port_io__oe), invert=False, direction=Direction.Bidir)")
+
+        port_i = SimulationPort("i", 3, invert=True)
+        self.assertEqual(port_i.direction, Direction.Input)
+        self.assertEqual(len(port_i), 3)
+        self.assertEqual(port_i.invert, (True, True, True))
+        self.assertIsInstance(port_i.i, Signal)
+        self.assertEqual(port_i.i.shape(), unsigned(3))
+        with self.assertRaisesRegex(AttributeError,
+                r"^Simulation port with input direction does not have an output signal$"):
+            port_i.o
+        with self.assertRaisesRegex(AttributeError,
+                r"^Simulation port with input direction does not have an output enable signal$"):
+            port_i.oe
+        self.assertRepr(port_i, "SimulationPort(i=(sig port_i__i), invert=True, direction=Direction.Input)")
+
+        port_o = SimulationPort("o", 2, invert=(True, False))
+        self.assertEqual(port_o.direction, Direction.Output)
+        self.assertEqual(len(port_o), 2)
+        self.assertEqual(port_o.invert, (True, False))
+        with self.assertRaisesRegex(AttributeError,
+                r"^Simulation port with output direction does not have an input signal$"):
+            port_o.i
+        self.assertIsInstance(port_o.o, Signal)
+        self.assertEqual(port_o.o.shape(), unsigned(2))
+        self.assertEqual(port_o.o.init, 0)
+        self.assertIsInstance(port_o.oe, Signal)
+        self.assertEqual(port_o.oe.shape(), unsigned(2))
+        self.assertEqual(port_o.oe.init, 0b11)
+        self.assertRepr(port_o, "SimulationPort(o=(sig port_o__o), oe=(sig port_o__oe), invert=(True, False), direction=Direction.Output)")
+
+    def test_construct_empty(self):
+        port_i = SimulationPort("i", 0, invert=True)
+        self.assertEqual(port_i.direction, Direction.Input)
+        self.assertEqual(len(port_i), 0)
+        self.assertEqual(port_i.invert, ())
+        self.assertIsInstance(port_i.i, Signal)
+        self.assertEqual(port_i.i.shape(), unsigned(0))
+        self.assertRepr(port_i, "SimulationPort(i=(sig port_i__i), invert=False, direction=Direction.Input)")
+
+    def test_name(self):
+        port = SimulationPort("io", 2, name="nyaa")
+        self.assertRepr(port, "SimulationPort(i=(sig nyaa__i), o=(sig nyaa__o), oe=(sig nyaa__oe), invert=False, direction=Direction.Bidir)")
+
+    def test_name_wrong(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Name must be a string, not 1$"):
+            SimulationPort("io", 1, name=1)
+
+    def test_construct_wrong(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Width must be a non-negative integer, not 'a'$"):
+            SimulationPort("io", "a")
+        with self.assertRaisesRegex(TypeError,
+                r"^Width must be a non-negative integer, not -1$"):
+            SimulationPort("io", -1)
+        with self.assertRaisesRegex(TypeError,
+                r"^'invert' must be a bool or iterable of bool, not 3$"):
+            SimulationPort("io", 1, invert=3)
+        with self.assertRaisesRegex(TypeError,
+                r"^'invert' must be a bool or iterable of bool, not \[1, 2\]$"):
+            SimulationPort("io", 2, invert=[1, 2])
+        with self.assertRaisesRegex(ValueError,
+                r"^Length of 'invert' \(2\) doesn't match port width \(1\)$"):
+            SimulationPort("io", 1, invert=(False, True))
+
+    def test_slice(self):
+        port_io = SimulationPort("io", 2)
+        self.assertRepr(port_io[0], "SimulationPort(i=(slice (sig port_io__i) 0:1), o=(slice (sig port_io__o) 0:1), oe=(slice (sig port_io__oe) 0:1), invert=False, direction=Direction.Bidir)")
+
+        port_i = SimulationPort("i", 3, invert=True)
+        self.assertRepr(port_i[1:3], "SimulationPort(i=(slice (sig port_i__i) 1:3), invert=True, direction=Direction.Input)")
+
+        port_o = SimulationPort("o", 2, invert=(True, False))
+        self.assertRepr(port_o[1], "SimulationPort(o=(slice (sig port_o__o) 1:2), oe=(slice (sig port_o__oe) 1:2), invert=False, direction=Direction.Output)")
+
+    def test_invert(self):
+        port_io = SimulationPort("io", 2)
+        self.assertRepr(~port_io, "SimulationPort(i=(sig port_io__i), o=(sig port_io__o), oe=(sig port_io__oe), invert=True, direction=Direction.Bidir)")
+
+        port_i = SimulationPort("i", 3, invert=True)
+        self.assertRepr(~port_i, "SimulationPort(i=(sig port_i__i), invert=False, direction=Direction.Input)")
+
+        port_o = SimulationPort("o", 2, invert=(True, False))
+        self.assertRepr(~port_o, "SimulationPort(o=(sig port_o__o), oe=(sig port_o__oe), invert=(False, True), direction=Direction.Output)")
+
+    def test_add(self):
+        port_io = SimulationPort("io", 2)
+        port_io2 = SimulationPort("io", 2)
+        port_i = SimulationPort("i", 3, invert=True)
+        port_o = SimulationPort("o", 2, invert=(True, False))
+
+        self.assertRepr(port_io + port_io2, "SimulationPort(i=(cat (sig port_io__i) (sig port_io2__i)), o=(cat (sig port_io__o) (sig port_io2__o)), oe=(cat (sig port_io__oe) (sig port_io2__oe)), invert=False, direction=Direction.Bidir)")
+        self.assertRepr(port_io + port_i, "SimulationPort(i=(cat (sig port_io__i) (sig port_i__i)), invert=(False, False, True, True, True), direction=Direction.Input)")
+        self.assertRepr(port_io + port_o, "SimulationPort(o=(cat (sig port_io__o) (sig port_o__o)), oe=(cat (sig port_io__oe) (sig port_o__oe)), invert=(False, False, True, False), direction=Direction.Output)")
+
+    def test_add_wrong(self):
+        io = IOPort(1)
+        with self.assertRaisesRegex(TypeError,
+                r"^unsupported operand type\(s\) for \+: 'SimulationPort' and 'SingleEndedPort'$"):
+            SimulationPort("io", 2) + SingleEndedPort(io)
+
+
 class BufferTestCase(FHDLTestCase):
     def test_signature(self):
         sig_i = Buffer.Signature("i", 4)
@@ -378,6 +503,121 @@ def test_elaborate_diff(self):
         )
         """)
 
+    def test_elaborate_sim(self):
+        port = SimulationPort("io", 4)
+        buf = Buffer("io", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.2:6)
+                (input 'oe' 0.6)
+                (input 'port__i' 0.7:11)
+                (output 'i' 5.0:4)
+                (output 'port__o' 0.2:6)
+                (output 'port__oe' (cat 0.6 0.6 0.6 0.6))
+            )
+            (cell 0 0 (top
+                (input 'o' 2:6)
+                (input 'oe' 6:7)
+                (input 'port__i' 7:11)
+                (output 'i' 5.0:4)
+                (output 'port__o' 0.2:6)
+                (output 'port__oe' (cat 0.6 0.6 0.6 0.6))
+            ))
+            (cell 1 0 (m 0.6 0.2 0.7))
+            (cell 2 0 (m 0.6 0.3 0.8))
+            (cell 3 0 (m 0.6 0.4 0.9))
+            (cell 4 0 (m 0.6 0.5 0.10))
+            (cell 5 0 (assignment_list 4'd0 (1 0:1 1.0) (1 1:2 2.0) (1 2:3 3.0) (1 3:4 4.0)))
+        )
+        """)
+
+        port = SimulationPort("io", 4, invert=[False, True, False, True])
+        buf = Buffer("io", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.2:6)
+                (input 'oe' 0.6)
+                (input 'port__i' 0.7:11)
+                (output 'i' 2.0:4)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 0.6 0.6 0.6 0.6))
+            )
+            (cell 0 0 (top
+                (input 'o' 2:6)
+                (input 'oe' 6:7)
+                (input 'port__i' 7:11)
+                (output 'i' 2.0:4)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 0.6 0.6 0.6 0.6))
+            ))
+            (cell 1 0 (^ 0.2:6 4'd10))
+            (cell 2 0 (^ 7.0:4 4'd10))
+            (cell 3 0 (m 0.6 1.0 0.7))
+            (cell 4 0 (m 0.6 1.1 0.8))
+            (cell 5 0 (m 0.6 1.2 0.9))
+            (cell 6 0 (m 0.6 1.3 0.10))
+            (cell 7 0 (assignment_list 4'd0 (1 0:1 3.0) (1 1:2 4.0) (1 2:3 5.0) (1 3:4 6.0)))
+        )
+        """)
+
+        buf = Buffer("i", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'port__i' 0.2:6)
+                (output 'i' 1.0:4)
+            )
+            (cell 0 0 (top
+                (input 'port__i' 2:6)
+                (output 'i' 1.0:4)
+            ))
+            (cell 1 0 (^ 0.2:6 4'd10))
+        )
+        """)
+
+        buf = Buffer("o", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.o, buf.oe, port.o, port.oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.2:6)
+                (input 'oe' 0.6)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 0.6 0.6 0.6 0.6))
+            )
+            (cell 0 0 (top
+                (input 'o' 2:6)
+                (input 'oe' 6:7)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 0.6 0.6 0.6 0.6))
+            ))
+            (cell 1 0 (^ 0.2:6 4'd10))
+        )
+        """)
+
+        # check that a port without `port.o`/`port.oe` works
+        port = SimulationPort("i", 4, invert=[False, True, False, True])
+        buf = Buffer("i", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'port__i' 0.2:6)
+                (output 'i' 1.0:4)
+            )
+            (cell 0 0 (top
+                (input 'port__i' 2:6)
+                (output 'i' 1.0:4)
+            ))
+            (cell 1 0 (^ 0.2:6 4'd10))
+        )
+        """)
+
 
 class FFBufferTestCase(FHDLTestCase):
     def test_signature(self):
@@ -616,6 +856,150 @@ def test_elaborate(self):
         )
         """)
 
+    def test_elaborate_sim(self):
+        port = SimulationPort("io", 4)
+        buf = FFBuffer("io", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.2:6)
+                (input 'oe' 0.6)
+                (input 'port__i' 0.7:11)
+                (input 'clk' 0.11)
+                (input 'rst' 0.12)
+                (output 'i' 5.0:4)
+                (output 'port__o' 6.0:4)
+                (output 'port__oe' (cat 7.0 7.0 7.0 7.0))
+            )
+            (module 1 0 ('top' 'io_buffer')
+                (input 'port__i' 0.7:11)
+                (input 'port__o' 6.0:4)
+                (input 'oe' 7.0)
+                (output 'i' 8.0:4)
+            )
+            (cell 0 0 (top
+                (input 'o' 2:6)
+                (input 'oe' 6:7)
+                (input 'port__i' 7:11)
+                (input 'clk' 11:12)
+                (input 'rst' 12:13)
+                (output 'i' 5.0:4)
+                (output 'port__o' 6.0:4)
+                (output 'port__oe' (cat 7.0 7.0 7.0 7.0))
+            ))
+            (cell 1 1 (m 7.0 6.0 0.7))
+            (cell 2 1 (m 7.0 6.1 0.8))
+            (cell 3 1 (m 7.0 6.2 0.9))
+            (cell 4 1 (m 7.0 6.3 0.10))
+            (cell 5 0 (flipflop 8.0:4 0 pos 0.11 0))
+            (cell 6 0 (flipflop 0.2:6 0 pos 0.11 0))
+            (cell 7 0 (flipflop 0.6 0 pos 0.11 0))
+            (cell 8 1 (assignment_list 4'd0 (1 0:1 1.0) (1 1:2 2.0) (1 2:3 3.0) (1 3:4 4.0)))
+        )
+        """)
+
+        port = SimulationPort("io", 4, invert=[False, True, False, True])
+        buf = FFBuffer("io", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.2:6)
+                (input 'oe' 0.6)
+                (input 'port__i' 0.7:11)
+                (input 'clk' 0.11)
+                (input 'rst' 0.12)
+                (output 'i' 7.0:4)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 9.0 9.0 9.0 9.0))
+            )
+            (module 1 0 ('top' 'io_buffer')
+                (input 'port__i' 0.7:11)
+                (output 'o_inv' 1.0:4)
+                (output 'i' 2.0:4)
+                (input 'o' 8.0:4)
+                (input 'oe' 9.0)
+            )
+            (cell 0 0 (top
+                (input 'o' 2:6)
+                (input 'oe' 6:7)
+                (input 'port__i' 7:11)
+                (input 'clk' 11:12)
+                (input 'rst' 12:13)
+                (output 'i' 7.0:4)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 9.0 9.0 9.0 9.0))
+            ))
+            (cell 1 1 (^ 8.0:4 4'd10))
+            (cell 2 1 (^ 10.0:4 4'd10))
+            (cell 3 1 (m 9.0 1.0 0.7))
+            (cell 4 1 (m 9.0 1.1 0.8))
+            (cell 5 1 (m 9.0 1.2 0.9))
+            (cell 6 1 (m 9.0 1.3 0.10))
+            (cell 7 0 (flipflop 2.0:4 0 pos 0.11 0))
+            (cell 8 0 (flipflop 0.2:6 0 pos 0.11 0))
+            (cell 9 0 (flipflop 0.6 0 pos 0.11 0))
+            (cell 10 1 (assignment_list 4'd0 (1 0:1 3.0) (1 1:2 4.0) (1 2:3 5.0) (1 3:4 6.0)))
+        )
+        """)
+
+        buf = FFBuffer("i", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'port__i' 0.2:6)
+                (input 'clk' 0.6)
+                (input 'rst' 0.7)
+                (output 'i' 2.0:4)
+            )
+            (module 1 0 ('top' 'io_buffer')
+                (input 'i_inv' 0.2:6)
+                (output 'i' 1.0:4)
+            )
+            (cell 0 0 (top
+                (input 'port__i' 2:6)
+                (input 'clk' 6:7)
+                (input 'rst' 7:8)
+                (output 'i' 2.0:4)
+            ))
+            (cell 1 1 (^ 0.2:6 4'd10))
+            (cell 2 0 (flipflop 1.0:4 0 pos 0.6 0))
+        )
+        """)
+
+        buf = FFBuffer("o", port)
+        nl = build_netlist(Fragment.get(buf, None), [buf.o, buf.oe, port.o, port.oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.2:6)
+                (input 'oe' 0.6)
+                (input 'clk' 0.7)
+                (input 'rst' 0.8)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 3.0 3.0 3.0 3.0))
+            )
+            (module 1 0 ('top' 'io_buffer')
+                (output 'o_inv' 1.0:4)
+                (input 'o' 2.0:4)
+                (input 'oe' 3.0)
+            )
+            (cell 0 0 (top
+                (input 'o' 2:6)
+                (input 'oe' 6:7)
+                (input 'clk' 7:8)
+                (input 'rst' 8:9)
+                (output 'port__o' 1.0:4)
+                (output 'port__oe' (cat 3.0 3.0 3.0 3.0))
+            ))
+            (cell 1 1 (^ 2.0:4 4'd10))
+            (cell 2 0 (flipflop 0.2:6 0 pos 0.7 0))
+            (cell 3 0 (flipflop 0.6 0 pos 0.7 0))
+        )
+        """)
+
 
 class DDRBufferTestCase(FHDLTestCase):
     def test_signature(self):
diff --git a/tests/test_lib_memory.py b/tests/test_lib_memory.py
index 3700613f3..ba828a8b3 100644
--- a/tests/test_lib_memory.py
+++ b/tests/test_lib_memory.py
@@ -329,7 +329,7 @@ def test_constructor_wrong(self):
             memory.Memory(shape="a", depth=3, init=[])
         with self.assertRaisesRegex(TypeError,
                 (r"^Memory initialization value at address 1: "
-                    r"'str' object cannot be interpreted as an integer$")):
+                    r"Initial value must be a constant-castable expression, not '0'$")):
             memory.Memory(shape=8, depth=4, init=[1, "0"])
         with self.assertRaisesRegex(ValueError,
                 r"^Either 'data' or 'shape' needs to be given$"):
@@ -373,7 +373,7 @@ def test_init_set_shapecastable(self):
     def test_init_set_wrong(self):
         m = memory.Memory(shape=8, depth=4, init=[])
         with self.assertRaisesRegex(TypeError,
-                r"^'str' object cannot be interpreted as an integer$"):
+                r"^Initial value must be a constant-castable expression, not 'a'$"):
             m.init[0] = "a"
         m = memory.Memory(shape=MyStruct, depth=4, init=[])
         # underlying TypeError message differs between PyPy and CPython
diff --git a/tests/test_lib_stream.py b/tests/test_lib_stream.py
index 17ce59684..ce45447e1 100644
--- a/tests/test_lib_stream.py
+++ b/tests/test_lib_stream.py
@@ -108,6 +108,11 @@ def test_eq(self):
         self.assertNotEqual(sig_av_ar,   sig_nav_nar)
         self.assertNotEqual(sig_av_ar,   sig_av_ar2)
 
+    def test_payload_init(self):
+        sig = stream.Signature(2, payload_init=0b10)
+        intf = sig.create()
+        self.assertEqual(intf.payload.init, 0b10)
+
     def test_interface_create_bad(self):
         with self.assertRaisesRegex(TypeError,
                 r"^Signature of stream\.Interface must be a stream\.Signature, not "
diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py
index d4b7d8cc5..eb2639047 100644
--- a/tests/test_lib_wiring.py
+++ b/tests/test_lib_wiring.py
@@ -635,7 +635,7 @@ def test_repr_inherit(self):
         class CustomInterface(PureInterface):
             pass
         intf = CustomInterface(Signature({}), path=())
-        self.assertRegex(repr(intf), r"^<CustomInterface: .+?>$")
+        self.assertRegex(repr(intf), r"^<.+\.CustomInterface: .+?>$")
 
 
 class FlippedInterfaceTestCase(unittest.TestCase):
@@ -1045,6 +1045,15 @@ def test_connect_multi_some_in_pairs(self):
                      a=Signal(),
                      b=Signal()))
 
+    def test_bug_1435_missing_module(self):
+        sig = Signature({"a": Out(1)})
+        p = sig.create()
+        q = sig.flip().create()
+
+        with self.assertRaisesRegex(TypeError,
+                r"^The initial argument must be a module, not <PureInterface: .+>$"):
+            connect(p, q)
+
 
 class ComponentTestCase(unittest.TestCase):
     def test_basic(self):
diff --git a/tests/test_sim.py b/tests/test_sim.py
index 612068d65..86e8e284b 100644
--- a/tests/test_sim.py
+++ b/tests/test_sim.py
@@ -727,8 +727,8 @@ def test_add_process_wrong(self):
     def test_add_process_wrong_generator(self):
         with self.assertSimulation(Module()) as sim:
             with self.assertRaisesRegex(TypeError,
-                    r"^Cannot add a process <.+?> because it is not an async function or "
-                    r"generator function$"):
+                    r"^Cannot add a process <.+?> because it is a generator object instead of "
+                    r"a function \(pass the function itself instead of calling it\)$"):
                 def process():
                     yield Delay()
                 sim.add_process(process())
@@ -743,12 +743,39 @@ def test_add_testbench_wrong(self):
     def test_add_testbench_wrong_generator(self):
         with self.assertSimulation(Module()) as sim:
             with self.assertRaisesRegex(TypeError,
-                    r"^Cannot add a testbench <.+?> because it is not an async function or "
-                    r"generator function$"):
+                    r"^Cannot add a testbench <.+?> because it is a generator object instead of "
+                    r"a function \(pass the function itself instead of calling it\)$"):
                 def testbench():
                     yield Delay()
                 sim.add_testbench(testbench())
 
+    def test_add_testbench_wrong_coroutine(self):
+        with self.assertSimulation(Module()) as sim:
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot add a testbench <.+?> because it is a coroutine object instead of "
+                    r"a function \(pass the function itself instead of calling it\)$"):
+                async def testbench():
+                    pass
+                sim.add_testbench(testbench())
+
+    def test_add_testbench_wrong_async_generator(self):
+        with self.assertSimulation(Module()) as sim:
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot add a testbench <.+?> because it is a generator object instead of "
+                    r"a function \(pass the function itself instead of calling it\)$"):
+                async def testbench():
+                    yield Delay()
+                sim.add_testbench(testbench())
+
+    def test_add_testbench_wrong_async_generator_func(self):
+        with self.assertSimulation(Module()) as sim:
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot add a testbench <.+?> because it is an async generator function "
+                    r"\(there is likely a stray `yield` in the function\)$"):
+                async def testbench():
+                    yield Delay()
+                sim.add_testbench(testbench)
+
     def test_add_clock_wrong_twice(self):
         m = Module()
         s = Signal()
@@ -1247,6 +1274,33 @@ async def testbench(ctx):
             [unknown]
         """))
 
+    def test_print_enum_followed_by(self):
+        class MyEnum(enum.Enum, shape=unsigned(2)):
+            A = 0
+            B = 1
+            CDE = 2
+
+        sig = Signal(MyEnum)
+        ctr = Signal(2)
+        m = Module()
+        m.d.comb += sig.eq(ctr)
+        m.d.sync += [
+            Print(Format("{} {}", sig, ctr)),
+            ctr.eq(ctr + 1),
+        ]
+        output = StringIO()
+        with redirect_stdout(output):
+            with self.assertSimulation(m) as sim:
+                sim.add_clock(1e-6, domain="sync")
+                async def testbench(ctx):
+                    await ctx.tick().repeat(4)
+                sim.add_testbench(testbench)
+        self.assertEqual(output.getvalue(), dedent("""\
+            A 0
+            B 1
+            CDE 2
+            [unknown] 3
+        """))
 
     def test_assert(self):
         m = Module()
@@ -1375,15 +1429,42 @@ class MyEnum(enum.Enum, shape=2):
         mem1 = MemoryData(shape=8, depth=4, init=[1, 2, 3])
         mem2 = MemoryData(shape=MyEnum, depth=4, init=[MyEnum.A, MyEnum.B, MyEnum.C])
         mem3 = MemoryData(shape=data.StructLayout({"a": signed(3), "b": 2}), depth=4, init=[{"a": 2, "b": 1}])
+        mem4 = MemoryData(shape=signed(8), depth=4, init=[1, -2, 3])
 
         async def testbench(ctx):
             await ctx.delay(1e-6)
             ctx.set(mem1[0], 4)
             ctx.set(mem2[3], MyEnum.C)
             ctx.set(mem3[2], {"a": -1, "b": 2})
+            ctx.set(mem4[1][4:], 0)
+            ctx.set(mem4[3][7], 1)
             await ctx.delay(1e-6)
+            self.assertEqual(ctx.get(mem4[1]), 0xe)
+            self.assertEqual(ctx.get(mem4[3]), -128)
 
-        with self.assertSimulation(Module(), traces=[mem1, mem2, mem3]) as sim:
+        with self.assertSimulation(Module(), traces=[mem1, mem2, mem3, mem4]) as sim:
+            sim.add_testbench(testbench)
+
+    def test_multiple_modules(self):
+        m = Module()
+        m.submodules.m1 = m1 = Module()
+        m.submodules.m2 = m2 = Module()
+        a = Signal(8)
+        b = Signal(8)
+        m1.d.comb += b[0:2].eq(a[0:2])
+        m1.d.comb += b[4:6].eq(a[4:6])
+        m2.d.comb += b[2:4].eq(a[2:4])
+        m2.d.comb += b[6:8].eq(a[6:8])
+        with self.assertSimulation(m) as sim:
+            async def testbench(ctx):
+                ctx.set(a, 0)
+                self.assertEqual(ctx.get(b), 0)
+                ctx.set(a, 0x12)
+                self.assertEqual(ctx.get(b), 0x12)
+                ctx.set(a, 0x34)
+                self.assertEqual(ctx.get(b), 0x34)
+                ctx.set(a, 0xdb)
+                self.assertEqual(ctx.get(b), 0xdb)
             sim.add_testbench(testbench)
 
 
@@ -1403,6 +1484,10 @@ def test_signal(self):
         a = Signal()
         self.assertDef(a, [a])
 
+    def test_expr(self):
+        a = Signal()
+        self.assertDef(a + 3, [a])
+
     def test_list(self):
         a = Signal()
         self.assertDef([a], [a])
@@ -2026,15 +2111,6 @@ async def testbench(ctx):
         self.assertTrue(reached_tb)
         self.assertTrue(reached_proc)
 
-    def test_bug_1363(self):
-        sim = Simulator(Module())
-        with self.assertRaisesRegex(TypeError,
-                r"^Cannot add a testbench <.+?> because it is not an async function or "
-                r"generator function$"):
-            async def testbench():
-                yield Delay()
-            sim.add_testbench(testbench())
-
     def test_issue_1368(self):
         sim = Simulator(Module())
         async def testbench(ctx):
diff --git a/tests/utils.py b/tests/utils.py
index a069f18fb..79f8f081b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,15 +16,57 @@
 
 
 class FHDLTestCase(unittest.TestCase):
+    maxDiff = None
+
     def assertRepr(self, obj, repr_str):
         if isinstance(obj, list):
             obj = Statement.cast(obj)
-        def prepare_repr(repr_str):
+        def squish_repr(repr_str):
             repr_str = re.sub(r"\s+",   " ",  repr_str)
             repr_str = re.sub(r"\( (?=\()", "(", repr_str)
             repr_str = re.sub(r"\) (?=\))", ")", repr_str)
             return repr_str.strip()
-        self.assertEqual(prepare_repr(repr(obj)), prepare_repr(repr_str))
+        def format_repr(input_repr, *, indent="    "):
+            output_repr = []
+            prefix = "\n"
+            name = None
+            index = 0
+            stack = []
+            current = ""
+            for char in input_repr:
+                if char == "(":
+                    stack.append((prefix, name, index))
+                    name, index = None, 0
+                    output_repr.append(char)
+                    if len(stack) == 1:
+                        prefix += indent
+                        output_repr.append(prefix)
+                elif char == ")":
+                    indented = (len(stack) == 1 or name in ("module", "top"))
+                    prefix, name, index = stack.pop()
+                    if indented:
+                        output_repr.append(prefix)
+                    output_repr.append(char)
+                elif char == " ":
+                    if name is None:
+                        name = current
+                        if name in ("module", "top"):
+                            prefix += indent
+                    else:
+                        index += 1
+                    current = ""
+                    if len(stack) == 1 or name == "module" and index >= 3 or name == "top":
+                        output_repr.append(prefix)
+                    else:
+                        output_repr.append(char)
+                elif name is None:
+                    current += char
+                    output_repr.append(char)
+                else:
+                    output_repr.append(char)
+            return "".join(output_repr)
+        # print("\n" + format_repr(squish_repr(repr(obj))))
+        self.assertEqual(format_repr(squish_repr(repr(obj))), format_repr(squish_repr(repr_str)))
 
     def assertFormal(self, spec, ports=None, mode="bmc", depth=1):
         stack = traceback.extract_stack()
@@ -62,7 +104,7 @@ def assertFormal(self, spec, ports=None, mode="bmc", depth=1):
         smtbmc
 
         [script]
-        read_ilang top.il
+        read_rtlil top.il
         prep
         {script}