From 91233efe454262261aecb137954c2e9e565aa67c Mon Sep 17 00:00:00 2001 From: Wanda Date: Wed, 11 Sep 2024 18:01:42 +0200 Subject: [PATCH] hdl: allow driving individual bits of signals from multiple modules or domains. Fixes #1454. --- amaranth/hdl/_dsl.py | 173 ++++++++++++++++++----------------- amaranth/hdl/_ir.py | 190 +++++++++++++++++++++++++++------------ docs/changes.rst | 14 ++- docs/guide.rst | 18 ++-- tests/test_back_rtlil.py | 1 - tests/test_hdl_dsl.py | 6 +- tests/test_hdl_ir.py | 123 ++++++++++++++++++++++++- 7 files changed, 362 insertions(+), 163 deletions(-) diff --git a/amaranth/hdl/_dsl.py b/amaranth/hdl/_dsl.py index 6de5b1e4f..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: @@ -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 ("",)) - other_mod_name = \ - ".".join(self.netlist.modules[driver.module_idx].name or ("",)) - 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 ("",)) + other_mod_name = \ + ".".join(self.netlist.modules[other_module_idx].name or ("",)) + 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 033daa2a1..58ca812f5 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 `_ +* `Amaranth 0.5.1 `_ * `Amaranth 0.5.0 `_ * `Amaranth 0.4.5 `_ * `Amaranth 0.4.4 `_ @@ -19,10 +21,20 @@ Documentation for past releases of the Amaranth language and toolchain is availa * `Amaranth 0.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 6268186cd..4923f0cfe 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[""] += ...`` 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 ` and :ref:`debug prints ` can be added using the same syntax. diff --git a/tests/test_back_rtlil.py b/tests/test_back_rtlil.py index a52f7ce94..e99bbdb4a 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 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 cc5517e60..e73a97272 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), [])