Repository: amaranth-lang/amaranth
Pull request #1513 (2024-09-18 02:49:58)

hdl: allow driving individual bits of signals from multiple modules or domains. [0.5 backport]


Patch:
diff --git a/amaranth/hdl/_dsl.py b/amaranth/hdl/_dsl.py
index 6de5b1e..9cf0b2b 100644
--- a/amaranth/hdl/_dsl.py
+++ b/amaranth/hdl/_dsl.py
@@ -19,82 +19,78 @@ from ._mem import MemoryData
 __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:
@@ -632,16 +628,27 @@ class Module(_ModuleBuilderRoot, Elaboratable):
 
             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 f24cd2e..90ddb8a 100644
--- a/amaranth/hdl/_ir.py
+++ b/amaranth/hdl/_ir.py
@@ -728,17 +728,40 @@ class NetlistDriver:
         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 @@ class NetlistEmitter:
         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 @@ class NetlistEmitter:
     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 @@ class NetlistEmitter:
             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/docs/changes.rst b/docs/changes.rst
index 033daa2..58ca812 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -9,6 +9,8 @@ Documentation for past releases
 
 Documentation for past releases of the Amaranth language and toolchain is available online:
 
+* `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/>`_
@@ -19,10 +21,20 @@ 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.2 (unreleased)
+Version 0.5.3 (unreleased)
 ==========================
 
 
+Language changes
+----------------
+
+* Added: individual bits of the same signal can now be assigned from different modules or domains.
+
+
+Version 0.5.2
+=============
+
+
 Standard library changes
 ------------------------
 
diff --git a/docs/guide.rst b/docs/guide.rst
index 6268186..4923f0c 100644
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -940,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::
 
@@ -949,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.
 


Test patch:
diff --git a/tests/test_back_rtlil.py b/tests/test_back_rtlil.py
index a52f7ce..e99bbdb 100644
--- a/tests/test_back_rtlil.py
+++ b/tests/test_back_rtlil.py
@@ -1214,7 +1214,6 @@ class FlopTestCase(RTLILTestCase):
             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
diff --git a/tests/test_hdl_dsl.py b/tests/test_hdl_dsl.py
index ecbed9d..0ab6b48 100644
--- a/tests/test_hdl_dsl.py
+++ b/tests/test_hdl_dsl.py
@@ -34,7 +34,6 @@ class DSLTestCase(FHDLTestCase):
         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 @@ class DSLTestCase(FHDLTestCase):
         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 @@ class DSLTestCase(FHDLTestCase):
         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 @@ class DSLTestCase(FHDLTestCase):
         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 @@ class DSLTestCase(FHDLTestCase):
     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 cc5517e..e73a972 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -265,7 +265,7 @@ class FragmentPortsTestCase(FHDLTestCase):
             (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 @@ class FragmentPortsTestCase(FHDLTestCase):
             (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 @@ class SwitchTestCase(FHDLTestCase):
         """)
 
 
+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 @@ class ConflictTestCase(FHDLTestCase):
         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 @@ class ConflictTestCase(FHDLTestCase):
         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), [])