From 3fd2c9734fb1e0f2f6e90ac8ad5ccb8c305e4c8f 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 1/2] `name_scope` is a context manager that prepends prefixes to `WireVector` and `MemBlock` names. This helps prevent name collisions in larger designs. This also defines `current_name_prefix`, which returns the current slash-delimited stack of name prefixes, and adds documentation and tests. --- docs/basic.rst | 33 ++++++++++- pyrtl/__init__.py | 12 +++- pyrtl/memory.py | 24 ++++++-- pyrtl/wire.py | 132 +++++++++++++++++++++++++++++++++++++++-- tests/test_memblock.py | 16 +++++ tests/test_wire.py | 49 +++++++++++++++ 6 files changed, 252 insertions(+), 14 deletions(-) diff --git a/docs/basic.rst b/docs/basic.rst index fcc0f2a9..38a2b37b 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -56,6 +56,33 @@ Constants :show-inheritance: :special-members: __init__ +.. _naming: + +Naming Best Practices +--------------------- + +All :class:`WireVectors<.WireVector>` and :class:`MemBlocks<.MemBlock>` in a +:class:`.Block` must have unique names. This requirement is enforced by +:meth:`.Block.sanity_check`. + +PyRTL imposes no other :class:`.WireVector` or :class:`.MemBlock` name +requirements, but this section covers some best practices for choosing names. +You can reduce the risk of name collisions by following these best practices + +Generally, it's best to use alphanumeric names that don't start with a number. +Names can include underscores (``_``). Avoid other symbols, especially: + +1. Slashes (``/``), which are used in names generated by :class:`.name_scope`. +2. Periods (``.``), which are used in names generated by :func:`.wire_struct`. +3. Square brackets (``[]``), which are used in names generated by :func:`.wire_matrix`. + +The :class:`.name_scope` context manager is recommended for creating unique +names based on code structure, which is less error-prone than manually +assembling names. + +.. autoclass:: pyrtl.name_scope +.. autofunction:: pyrtl.current_name_prefix + .. _conditional_assignment: Conditional Assignment @@ -137,7 +164,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 @@ -190,8 +217,8 @@ These default values can be changed by passing a ``defaults`` dict to :data:`.conditional_assignment` ``defaults`` are not supported for :class:`.MemBlock`. -The Conditional Assigment Operator (``|=``) -------------------------------------------- +Conditional Assigment Operator (``|=``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Conditional assignments are written with the ``|=`` (:meth:`~.WireVector.__ior__`) operator, and not the usual ``<<=`` diff --git a/pyrtl/__init__.py b/pyrtl/__init__.py index 923d95ad..58000718 100644 --- a/pyrtl/__init__.py +++ b/pyrtl/__init__.py @@ -17,7 +17,15 @@ ) # convenience classes for building hardware -from .wire import WireVector, Input, Output, Const, Register +from .wire import ( + WireVector, + Input, + Output, + Const, + Register, + name_scope, + current_name_prefix, +) from .gate_graph import GateGraph, Gate @@ -161,6 +169,8 @@ "Output", "Const", "Register", + "name_scope", + "current_name_prefix", # gate_graph "GateGraph", "Gate", diff --git a/pyrtl/memory.py b/pyrtl/memory.py index b61174d9..45231bed 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, +) # ------------------------------------------------------------------------ # @@ -247,13 +253,13 @@ class EnabledWrite(NamedTuple): """Single-bit :class:`.WireVector` indicating if a write should occur.""" id: int - """A unique integer assigned to each MemBlock. + """A unique integer assigned to the ``MemBlock``. .. doctest only:: >>> import pyrtl >>> pyrtl.reset_working_block() - >>> pyrtl.memory._memIndex.internal_index = 0 + >>> pyrtl.memory._reset_memory_indexer() Example:: @@ -266,6 +272,16 @@ class EnabledWrite(NamedTuple): 1 """ + name: str + """A unique name for the ``MemBlock``. + + .. note:: + + All :class:`MemBlocks` in a :class:`Block` must have unique names. The + :class:`.name_scope` context manager helps avoid name collisions, and the + :ref:`naming` section covers best practices for choosing names in PyRTL. + """ + def __init__( self, bitwidth: int, @@ -294,7 +310,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 c5f88bbc..d910886b 100644 --- a/pyrtl/wire.py +++ b/pyrtl/wire.py @@ -57,6 +57,119 @@ 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 in code that runs more than once. Example:: + + >>> def make_counter(prefix: str) -> pyrtl.Register: + ... with pyrtl.name_scope(prefix): + ... counter = pyrtl.Register(name="counter", bitwidth=8) + ... counter.next <<= counter + 1 + ... return counter + + >>> counter_a = make_counter(prefix="a") + >>> counter_b = make_counter(prefix="b") + + >>> 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:: + + >>> with pyrtl.name_scope("foo"): + ... wire = pyrtl.WireVector(name="wire", bitwidth=7) + ... with pyrtl.name_scope("bar"): + ... const = pyrtl.Const(name="const", val=1) + + >>> wire.name + 'foo/wire' + >>> const.name + 'foo/bar/const' + + .. note:: + + ``name_scope`` helps avoid name collisions, but it can not prevent them. + ``name_scope`` just prepends string prefixes to names, and these prefixes have + no special properties. For example, we could create a :class:`WireVector` whose + name collides with the previous example's :class:`Const`:: + + >>> collider = pyrtl.WireVector(name="foo/bar/const", bitwidth=4) + >>> pyrtl.working_block().sanity_check() + Traceback (most recent call last): + ... + pyrtl.pyrtlexceptions.PyrtlError: Duplicate wire names found for the following different signals: ['foo/bar/const'] (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 PyrtlInternalError(msg) + + removed_prefix = _name_prefix_stack.pop() + if removed_prefix != self.prefix: + msg = "Corrupted name prefix stack" + raise PyrtlInternalError(msg) + + +def current_name_prefix(suffix: str = "") -> str: + """Return ``suffix`` prepended with the current slash-delimited name prefix stack. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + Examples:: + + >>> with pyrtl.name_scope("a"): + ... current_name_prefix() + 'a/' + + >>> with pyrtl.name_scope("a"): + ... with pyrtl.name_scope("b"): + ... current_name_prefix("c") + 'a/b/c' + + :param suffix: Suffix to append to the current stack of name prefixes. If no + ``suffix`` is provided, returns the current stack of slash-delimited name + prefixes, with a trailing slash. + """ + if not _name_prefix_stack: + return suffix + + joined_prefixes = "/".join(_name_prefix_stack) + return f"{joined_prefixes}/{suffix}" + + class WireVector: """The main class for describing the connections between operators. @@ -322,7 +435,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: @@ -330,7 +443,7 @@ def __init__( @property def name(self) -> str: - """A property holding the name of the :class:`WireVector`. + """A property holding the :class:`WireVector`'s unique name. .. doctest only:: @@ -339,12 +452,19 @@ def name(self) -> str: The name can be read or written. Examples:: - >>> a = WireVector(name="foo", bitwidth=1) - >>> a.name + >>> foo = WireVector(name="foo", bitwidth=1) + >>> foo.name 'foo' - >>> a.name = "mywire" - >>> a.name + >>> foo.name = "mywire" + >>> foo.name 'mywire' + + .. note:: + + All :class:`WireVectors` in a :class:`Block` must have unique + names. The :class:`.name_scope` context manager helps avoid name collisions, + and the :ref:`naming` section covers best practices for choosing names in + PyRTL. """ return self._name 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 7bcc4db1..f9146f60 100644 --- a/tests/test_wire.py +++ b/tests/test_wire.py @@ -55,6 +55,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 @@ -95,6 +98,52 @@ def test_valid_name_setter(self): self.check_name_setter(str(24)) self.check_name_setter("twenty_four") + def test_name_scope(self): + """Test ``name_scope`` and ``current_name_prefix``.""" + with pyrtl.name_scope("a"): + a_scope = pyrtl.current_name_prefix() + with pyrtl.name_scope("b"): + b_scope = pyrtl.current_name_prefix() + c = pyrtl.WireVector(name="c") + a_scope_2 = pyrtl.current_name_prefix() + d = pyrtl.WireVector(name="d") + + with pyrtl.name_scope("e"): + e_scope = pyrtl.current_name_prefix() + f = pyrtl.Register(name="f") + g = pyrtl.Const(42) + + with pyrtl.name_scope("h"): + h_scope = pyrtl.current_name_prefix() + i = pyrtl.Input(name="i") + j = pyrtl.Output(name="j") + + self.assertEqual(a_scope, "a/") + self.assertEqual(b_scope, "a/b/") + self.assertEqual(a_scope, a_scope_2) + + self.assertEqual(e_scope, "a/e/") + self.assertEqual(h_scope, "a/e/h/") + + 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.PyrtlInternalError): + 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.PyrtlInternalError): + with pyrtl.name_scope("foo"): + pyrtl.WireVector(name="w") + pyrtl.wire._name_prefix_stack[-1] = "bar" + class TestWireVectorFail(unittest.TestCase): def setUp(self): From abf32479dab7818d27632fa55e193a2a79166cf1 Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:04:11 -0700 Subject: [PATCH 2/2] Minor documentation improvements. --- docs/basic.rst | 6 ++++-- pyrtl/wire.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/basic.rst b/docs/basic.rst index 38a2b37b..b505f118 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -67,10 +67,12 @@ All :class:`WireVectors<.WireVector>` and :class:`MemBlocks<.MemBlock>` in a PyRTL imposes no other :class:`.WireVector` or :class:`.MemBlock` name requirements, but this section covers some best practices for choosing names. -You can reduce the risk of name collisions by following these best practices +You can reduce the risk of name collisions by following these best practices. Generally, it's best to use alphanumeric names that don't start with a number. -Names can include underscores (``_``). Avoid other symbols, especially: +Names can include underscores (``_``). These guidelines help preserve PyRTL +names when exporting to other languages, like Verilog with +:func:`.output_to_verilog`. Avoid other symbols, especially: 1. Slashes (``/``), which are used in names generated by :class:`.name_scope`. 2. Periods (``.``), which are used in names generated by :func:`.wire_struct`. diff --git a/pyrtl/wire.py b/pyrtl/wire.py index d910886b..61e3c493 100644 --- a/pyrtl/wire.py +++ b/pyrtl/wire.py @@ -141,7 +141,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def current_name_prefix(suffix: str = "") -> str: - """Return ``suffix`` prepended with the current slash-delimited name prefix stack. + """Return the current stack of slash-delimited name prefixes. .. doctest only:: @@ -159,7 +159,7 @@ def current_name_prefix(suffix: str = "") -> str: ... current_name_prefix("c") 'a/b/c' - :param suffix: Suffix to append to the current stack of name prefixes. If no + :param suffix: A suffix to append to the current stack of name prefixes. If no ``suffix`` is provided, returns the current stack of slash-delimited name prefixes, with a trailing slash. """