From 8e4ef9d0b31ce01eb121426cc88bd0cec0ab2366 Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:07:33 -0700 Subject: [PATCH] `name_scope` is a context manager that prepends prefixes to `WireVector` and `MemBlock` names. This helps prevent name collisions in larger designs. --- docs/basic.rst | 13 +++++- pyrtl/__init__.py | 3 +- pyrtl/memory.py | 10 ++++- pyrtl/wire.py | 91 +++++++++++++++++++++++++++++++++++++++++- tests/test_memblock.py | 16 ++++++++ tests/test_wire.py | 36 +++++++++++++++++ 6 files changed, 163 insertions(+), 6 deletions(-) diff --git a/docs/basic.rst b/docs/basic.rst index fcc0f2a9..505fc8bd 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -137,7 +137,7 @@ Context manager implementing PyRTL's ``otherwise`` under :data:`.conditional_ass .. _conditional_assignment_defaults: Conditional Assignment Defaults -------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Every PyRTL wire, register, and memory must have a value in every cycle. PyRTL does not support "don't care" or ``X`` values. To satisfy this requirement, conditional @@ -191,7 +191,7 @@ These default values can be changed by passing a ``defaults`` dict to :class:`.MemBlock`. The Conditional Assigment Operator (``|=``) -------------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Conditional assignments are written with the ``|=`` (:meth:`~.WireVector.__ior__`) operator, and not the usual ``<<=`` @@ -238,3 +238,12 @@ In this example the ``<<=`` in ``make_adder`` should be unconditional, even thou assignment to ``w`` is already conditional. Making the lower-level assignment to ``output`` conditional would not make sense, especially if ``output`` is used elsewhere in the circuit. + +Naming +------ + +All :class:`WireVectors<.WireVector>` and :class:`MemBlocks<.MemBlock>` in a +:class:`.Block` must have unique names. The :class:`.name_scope` context manager +helps make unique names: + +.. autoclass:: pyrtl.name_scope diff --git a/pyrtl/__init__.py b/pyrtl/__init__.py index 923d95ad..ff30273f 100644 --- a/pyrtl/__init__.py +++ b/pyrtl/__init__.py @@ -17,7 +17,7 @@ ) # convenience classes for building hardware -from .wire import WireVector, Input, Output, Const, Register +from .wire import WireVector, Input, Output, Const, Register, name_scope from .gate_graph import GateGraph, Gate @@ -161,6 +161,7 @@ "Output", "Const", "Register", + "name_scope", # gate_graph "GateGraph", "Gate", diff --git a/pyrtl/memory.py b/pyrtl/memory.py index b61174d9..de22f531 100644 --- a/pyrtl/memory.py +++ b/pyrtl/memory.py @@ -26,7 +26,13 @@ from pyrtl.corecircuits import as_wires from pyrtl.helperfuncs import infer_val_and_bitwidth from pyrtl.pyrtlexceptions import PyrtlError -from pyrtl.wire import Const, WireVector, WireVectorLike, next_tempvar_name +from pyrtl.wire import ( + Const, + WireVector, + WireVectorLike, + current_name_prefix, + next_tempvar_name, +) # ------------------------------------------------------------------------ # @@ -294,7 +300,7 @@ def __init__( self.max_read_ports = max_read_ports self.num_read_ports = 0 self.block = working_block(block) - name = next_tempvar_name(name) + name = current_name_prefix(next_tempvar_name(name)) if bitwidth <= 0: msg = "bitwidth must be >= 1" diff --git a/pyrtl/wire.py b/pyrtl/wire.py index 6abef0c1..c6944555 100644 --- a/pyrtl/wire.py +++ b/pyrtl/wire.py @@ -57,6 +57,95 @@ def next_tempvar_name(name=""): return name +_name_prefix_stack: list[str] = [] +""" +Stack of current name prefixes. :func:`current_name_prefix` will prepend these prefixes +to the names of all newly created :class:`WireVectors` and +:class:`MemBlocks<.MemBlock>`. +""" + + +class name_scope: + """Context manager that prepends a slash-delimited ``prefix`` to the names of all + :class:`WireVectors` and :class:`MemBlocks<.MemBlock>` created within. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + This context manager helps create unique :class:`WireVector` and :class:`.MemBlock` + names when the code that makes hardware runs more than once. Example:: + + >>> def make_counter(prefix: str, bitwidth: int) -> pyrtl.Register: + ... with pyrtl.name_scope(prefix): + ... counter = pyrtl.Register(name="counter", bitwidth=bitwidth) + ... counter.next <<= counter + 1 + ... return counter + + >>> counter_a = make_counter(prefix="a", bitwidth=2) + >>> counter_b = make_counter(prefix="b", bitwidth=3) + + >>> counter_a.name + 'a/counter' + >>> counter_b.name + 'b/counter' + + Without ``name_scope``, the code above would fail :meth:`Block.sanity_check`, + because there would be two :class:`Registers` named ``counter``. + ``name_scope`` can be composed arbitrarily:: + + >>> with pyrtl.name_scope("foo"): + ... with pyrtl.name_scope("bar"): + ... baz = pyrtl.WireVector(name="baz", bitwidth=2) + + >>> baz.name + 'foo/bar/baz' + + .. note:: + + ``name_scope`` helps avoid name collisions, but it can not prevent them. + ``name_scope`` just prepends prefixes to names, and these prefixes have no + special properties. For example, we could create a :class:`Register` that + collides with the previous example's ``foo/bar/baz`` :class:`WireVector`:: + + >>> reg = pyrtl.Register(name="foo/bar/baz", bitwidth=3) + >>> pyrtl.working_block().sanity_check() + Traceback (most recent call last): + ... + pyrtl.pyrtlexceptions.PyrtlError: Duplicate wire names found for the following different signals: ['foo/bar/baz'] (make sure you are not using "tmp" or "const_" as a signal name because those are reserved for internal use) + + :param prefix: Prefix to prepend to the names of all + :class:`WireVectors` and :class:`MemBlocks<.MemBlock>` created + within. + """ # noqa: E501 + + def __init__(self, prefix: str): + self.prefix = prefix + + def __enter__(self): + _name_prefix_stack.append(self.prefix) + + def __exit__(self, exc_type, exc_val, exc_tb): + if not _name_prefix_stack: + msg = "Unexpected empty name prefix stack" + raise PyrtlError(msg) + + removed_prefix = _name_prefix_stack.pop() + if removed_prefix != self.prefix: + msg = "Corrupted name prefix stack" + raise PyrtlError(msg) + + +def current_name_prefix(name: str) -> str: + """Return ``name`` prepended with the current ``_name_prefix_stack``.""" + if not _name_prefix_stack: + return name + + joined_prefixes = "/".join(_name_prefix_stack) + return f"{joined_prefixes}/{name}" + + class WireVector: """The main class for describing the connections between operators. @@ -322,7 +411,7 @@ def __init__( # used only to verify the one to one relationship of wires and blocks self._block = working_block(block) - self.name = next_tempvar_name(name) + self.name = current_name_prefix(next_tempvar_name(name)) self._validate_bitwidth(bitwidth) if core._setting_keep_wirevector_call_stack: diff --git a/tests/test_memblock.py b/tests/test_memblock.py index dda6ead8..2e83a851 100644 --- a/tests/test_memblock.py +++ b/tests/test_memblock.py @@ -538,5 +538,21 @@ def test_unsigned_romdata(self): self.assertEqual(actual_read_data, romdata[i]) +class TestMemBlockNaming(unittest.TestCase): + def setUp(self): + pyrtl.reset_working_block() + + def test_memblock_naming(self): + with pyrtl.name_scope("a"): + mem = pyrtl.MemBlock(name="mem", addrwidth=2, bitwidth=3) + with pyrtl.name_scope("b"): + rom = pyrtl.RomBlock( + name="rom", addrwidth=2, bitwidth=3, romdata=list(range(4)) + ) + + self.assertEqual(mem.name, "a/mem") + self.assertEqual(rom.name, "a/b/rom") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_wire.py b/tests/test_wire.py index 24574911..3022f45d 100644 --- a/tests/test_wire.py +++ b/tests/test_wire.py @@ -64,6 +64,9 @@ def test_rename(self): class TestWireVectorNames(unittest.TestCase): + def setUp(self): + pyrtl.reset_working_block() + def is_valid_str(self, s): return pyrtl.wire.next_tempvar_name(s) == s @@ -104,6 +107,39 @@ def test_valid_name_setter(self): self.check_name_setter(str(24)) self.check_name_setter("twenty_four") + def test_name_scope(self): + with pyrtl.name_scope("a"): + with pyrtl.name_scope("b"): + c = pyrtl.WireVector(name="c") + d = pyrtl.WireVector(name="d") + + with pyrtl.name_scope("e"): + f = pyrtl.Register(name="f") + g = pyrtl.Const(42) + + with pyrtl.name_scope("h"): + i = pyrtl.Input(name="i") + j = pyrtl.Output(name="j") + + self.assertEqual(c.name, "a/b/c") + self.assertEqual(d.name, "a/d") + self.assertEqual(f.name, "a/e/f") + self.assertTrue(g.name.startswith("a/e/")) + self.assertEqual(i.name, "a/e/h/i") + self.assertEqual(j.name, "a/e/h/j") + + def test_name_scope_error_empty_stack(self): + with self.assertRaises(pyrtl.PyrtlError): + with pyrtl.name_scope("foo"): + pyrtl.WireVector(name="w") + pyrtl.wire._name_prefix_stack.pop() + + def test_name_scope_error_corrupted_stack(self): + with self.assertRaises(pyrtl.PyrtlError): + with pyrtl.name_scope("foo"): + pyrtl.WireVector(name="w") + pyrtl.wire._name_prefix_stack[-1] = "bar" + class TestWireVectorFail(unittest.TestCase): def setUp(self):