Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ``<<=``
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion pyrtl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -161,6 +161,7 @@
"Output",
"Const",
"Register",
"name_scope",
# gate_graph
"GateGraph",
"Gate",
Expand Down
10 changes: 8 additions & 2 deletions pyrtl/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

# ------------------------------------------------------------------------
#
Expand Down Expand Up @@ -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"
Expand Down
91 changes: 90 additions & 1 deletion pyrtl/wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<WireVector>` and
:class:`MemBlocks<.MemBlock>`.
"""


class name_scope:
"""Context manager that prepends a slash-delimited ``prefix`` to the names of all
:class:`WireVectors<WireVector>` 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<Register>` 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<WireVector>` 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.

Expand Down Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_memblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
36 changes: 36 additions & 0 deletions tests/test_wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
Loading