Skip to content
Merged
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
35 changes: 32 additions & 3 deletions docs/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,35 @@ 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 (``_``). 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`.
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
Expand Down Expand Up @@ -137,7 +166,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 @@ -190,8 +219,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 ``<<=``
Expand Down
12 changes: 11 additions & 1 deletion pyrtl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -161,6 +169,8 @@
"Output",
"Const",
"Register",
"name_scope",
"current_name_prefix",
# gate_graph
"GateGraph",
"Gate",
Expand Down
24 changes: 20 additions & 4 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 @@ -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::

Expand All @@ -266,6 +272,16 @@ class EnabledWrite(NamedTuple):
1
"""

name: str
"""A unique name for the ``MemBlock``.

.. note::

All :class:`MemBlocks<MemBlock>` 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,
Expand Down Expand Up @@ -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"
Expand Down
132 changes: 126 additions & 6 deletions pyrtl/wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 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<Register>` 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<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 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 the current stack of slash-delimited name prefixes.

.. 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: 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.
"""
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.

Expand Down Expand Up @@ -322,15 +435,15 @@ 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:
self.init_call_stack = traceback.format_stack()

@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::

Expand All @@ -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<WireVector>` 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

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

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