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
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]

runs-on: ${{ matrix.os }}

Expand Down
2 changes: 1 addition & 1 deletion docs/simtest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Wave Renderer
:show-inheritance:
.. autoclass:: pyrtl.simulation.Utf8AltRendererConstants
:show-inheritance:
.. autoclass:: pyrtl.simulation.Cp437RendererConstants
.. autoclass:: pyrtl.simulation.Utf8BasicRendererConstants
:show-inheritance:
.. autoclass:: pyrtl.simulation.AsciiRendererConstants
:show-inheritance:
2 changes: 1 addition & 1 deletion examples/introduction-to-hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def software_fibonacci(n: int):
# `software_fibonacci` is an iterative Fibonacci implementation. It repeatedly adds `a`
# and `b` to calculate the `n`th number in the sequence.
print("n software_fibonacci(n)")
print("─────────────────────────")
print("-------------------------")
for n in range(10):
print(n, " ", software_fibonacci(n))

Expand Down
31 changes: 23 additions & 8 deletions examples/renderer-demo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# # Render traces with various `WaveRenderer` options.

# Run this demo to see which options work well in your terminal.
import sys

import pyrtl


Expand Down Expand Up @@ -50,25 +52,38 @@ def make_counter(period: int, bitwidth: int = 2):

# Render the trace with a variety of rendering options.
renderers = {
"powerline": (
pyrtl.simulation.PowerlineRendererConstants(),
"Requires a font with powerline glyphs",
"ascii": (
pyrtl.simulation.AsciiRendererConstants(),
"Basic 7-bit ASCII renderer",
),
}
unicode_renderers = {
"utf-8": (
pyrtl.simulation.Utf8RendererConstants(),
"Unicode, default non-Windows renderer",
"Unicode (default)",
),
"utf-8-alt": (
pyrtl.simulation.Utf8AltRendererConstants(),
"Unicode, alternate display option",
),
"cp437": (
pyrtl.simulation.Cp437RendererConstants(),
"Code page 437 (8-bit ASCII), default Windows renderer",
"utf-8-basic": (
pyrtl.simulation.Utf8BasicRendererConstants(),
"Unicode, basic display option",
),
"powerline": (
pyrtl.simulation.PowerlineRendererConstants(),
"Requires a font with powerline glyphs",
),
"ascii": (pyrtl.simulation.AsciiRendererConstants(), "Basic 7-bit ASCII renderer"),
}

if sys.stdout.encoding == "utf-8":
renderers = renderers | unicode_renderers
else:
print(
"Terminal does not support UTF-8! Detected encoding: "
f"{sys.stdout.encoding}. Disabling UTF-8 renderers."
)

for name, (constants, notes) in renderers.items():
print(f"# {notes}")
print(f"export PYRTL_RENDERER={name}\n")
Expand Down
2 changes: 1 addition & 1 deletion ipynb-examples/introduction-to-hardware.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"outputs": [],
"source": [
"print(\"n software_fibonacci(n)\")\n",
"print(\"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\")\n",
"print(\"-------------------------\")\n",
"for n in range(10):\n",
" print(n, \" \", software_fibonacci(n))\n"
]
Expand Down
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# PyRTL uses `just` instead of `make` because:
# * `make` is not installed by default on Windows.
# * `uv` can install `just` on all supported platforms from PyPI.

set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]

presubmit: tests docs

tests:
Expand Down
20 changes: 10 additions & 10 deletions pyrtl/helperfuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,9 +753,7 @@ def val_to_signed_integer(value: int, bitwidth: int) -> int:

.. doctest only::

>>> import os
>>> import pyrtl
>>> os.environ["PYRTL_RENDERER"] = "cp437"
>>> pyrtl.reset_working_block()

Reinterpret an unsigned integer (not a :class:`WireVector`!) as a signed integer.
Expand All @@ -767,16 +765,18 @@ def val_to_signed_integer(value: int, bitwidth: int) -> int:
``val_to_signed_integer`` can also be used as an ``repr_func`` for
:meth:`~SimulationTrace.render_trace`, to display signed integers in traces::

>>> bitwidth = 3
>>> counter = pyrtl.Register(name="counter", bitwidth=bitwidth)
>>> counter.next <<= counter + 1
counter = pyrtl.Register(name="counter", bitwidth=3, reset_value=-4)
counter.next <<= pyrtl.signed_add(counter, 1)

sim = pyrtl.Simulation()
sim.step_multiple(nsteps=2 ** counter.bitwidth)
sim.tracer.render_trace(repr_func=val_to_signed_integer)

will display::

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=2 ** bitwidth)
>>> sim.tracer.render_trace(repr_func=val_to_signed_integer)
│0 │1 │2 │3 │4 │5 │6 │7
<BLANKLINE>
counter ──┤1 │2 │3 │-4│-3│-2│-1

counter -4│-3│-2│-1├──┤1 │2 │3

:func:`infer_val_and_bitwidth` performs the opposite conversion::

Expand Down
90 changes: 69 additions & 21 deletions pyrtl/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import numbers
import os
import re
import sys
import warnings
from collections.abc import Callable, Mapping
from dataclasses import dataclass
Expand Down Expand Up @@ -73,6 +74,32 @@ class Simulation:

- ``.memvalue``: a map from :attr:`MemBlock.id` (``memid``) to a dictionary of
``{address: value}``.

.. doctest only::

>>> import os
>>> import pyrtl
>>> os.environ["PYRTL_RENDERER"] = "utf-8-basic"
>>> pyrtl.reset_working_block()

Example::

>>> a = pyrtl.Input(name="a", bitwidth=8)
>>> b = pyrtl.Input(name="b", bitwidth=8)
>>> sum = pyrtl.Output(name="sum", bitwidth=9)
>>> sum <<= a + b

>>> sim = pyrtl.Simulation()
>>> sim.step({"a": 1, "b": 2})
>>> sim.step({"a": 3, "b": 4})
>>> sim.tracer.render_trace()
│0 │1
<BLANKLINE>
a 0x1│0x3
<BLANKLINE>
b 0x2│0x4
<BLANKLINE>
sum 0x3│0x7
"""

tracer: SimulationTrace
Expand All @@ -83,7 +110,7 @@ class Simulation:

>>> import os
>>> import pyrtl
>>> os.environ["PYRTL_RENDERER"] = "cp437"
>>> os.environ["PYRTL_RENDERER"] = "utf-8-basic"
>>> pyrtl.reset_working_block()

``tracer`` is typically used to render simulation waveforms with
Expand Down Expand Up @@ -233,11 +260,26 @@ def step(self, provided_inputs: dict[str, int] | None = None):

All :class:`Input` wires must be in the ``provided_inputs``.

Example: if we have :class:`Inputs<Input>` named ``a`` and ``x``, we can call::
.. doctest only::

sim.step({'a': 1, 'x': 23})
>>> import pyrtl
>>> pyrtl.reset_working_block()

to simulate a cycle where ``a == 1`` and ``x == 23`` respectively.
Example::

>>> a = pyrtl.Input(name="a", bitwidth=8)
>>> b = pyrtl.Input(name="b", bitwidth=8)
>>> sum = pyrtl.Output(name="sum", bitwidth=9)
>>> sum <<= a + b

>>> sim = pyrtl.Simulation()
>>> sim.step({"a": 1, "b": 2})
>>> sim.inspect("sum")
3

>>> sim.step({"a": 3, "b": 4})
>>> sim.inspect("sum")
7

:param provided_inputs: A dictionary mapping :class:`Input`
:class:`WireVectors<WireVector>` to their values for this step.
Expand Down Expand Up @@ -1272,8 +1314,8 @@ class RendererConstants:

.. inheritance-diagram:: pyrtl.simulation.Utf8RendererConstants
pyrtl.simulation.Utf8AltRendererConstants
pyrtl.simulation.Utf8BasicRendererConstants
pyrtl.simulation.PowerlineRendererConstants
pyrtl.simulation.Cp437RendererConstants
pyrtl.simulation.AsciiRendererConstants
:parts: 1

Expand Down Expand Up @@ -1429,26 +1471,25 @@ class PowerlineRendererConstants(Utf8RendererConstants):
_zero = "─"


class Cp437RendererConstants(RendererConstants):
"""Code page 437 renderer constants (for windows ``cmd`` compatibility).
class Utf8BasicRendererConstants(RendererConstants):
"""Basic Unicode renderer constants that does not use ANSI escape codes.

Single-bit :class:`WireVectors<WireVector>` are rendered as square waveforms, with
vertical rising and falling edges. Multi-bit :class:`WireVector` values are rendered
between vertical bars.

`Code page 437 <https://en.wikipedia.org/wiki/Code_page_437>`_ is also known as
8-bit ASCII. This is the default renderer on Windows platforms.

Compared to :class:`Utf8RendererConstants`, this renderer is more compact because it
uses one character between cycles instead of two, but the wire names are vertically
aligned at the bottom of each waveform.
aligned at the bottom of each waveform. This renderer does not use any `ANSI escape
codes <https://en.wikipedia.org/wiki/ANSI_escape_code>`_, which makes its output
suitable for inclusion in text files.

Enable this renderer by default by setting the ``PYRTL_RENDERER`` environment
variable to ``cp437``::
variable to ``utf-8-basic``::

export PYRTL_RENDERER=cp437
export PYRTL_RENDERER=utf-8-basic

.. image:: ../docs/screenshots/pyrtl-renderer-demo-cp437.png
.. image:: ../docs/screenshots/pyrtl-renderer-demo-utf-8-basic.png
"""

_tick = "│"
Expand Down Expand Up @@ -1501,18 +1542,25 @@ def default_renderer() -> WaveRenderer:
if "PYRTL_RENDERER" in os.environ:
# Use user-specified renderer constants.
renderer = os.environ["PYRTL_RENDERER"]
elif "PROMPT" in os.environ:
# Windows Command Prompt, use code page 437 renderer constants.
renderer = "cp437"
else:
elif sys.stdout.encoding == "utf-8":
# Use UTF-8 renderer constants by default.
renderer = "utf-8"
else:
warnings.warn(
"Terminal does not support UTF-8! Detected encoding: "
f"{sys.stdout.encoding}. Defaulting to ASCII renderer.",
RuntimeWarning,
stacklevel=2,
)
renderer = "ascii"

utf_8_basic = Utf8BasicRendererConstants()
renderer_map = {
"powerline": PowerlineRendererConstants(),
"utf-8": Utf8RendererConstants(),
"utf-8-alt": Utf8AltRendererConstants(),
"cp437": Cp437RendererConstants(),
"utf-8-basic": utf_8_basic,
"cp437": utf_8_basic, # For backwards compatibility.
"ascii": AsciiRendererConstants(),
}

Expand Down Expand Up @@ -1851,7 +1899,7 @@ def render_trace(

>>> import os
>>> import pyrtl
>>> os.environ["PYRTL_RENDERER"] = "cp437"
>>> os.environ["PYRTL_RENDERER"] = "utf-8-basic"
>>> pyrtl.reset_working_block()

The resulting output can be viewed directly in a terminal. Example::
Expand Down Expand Up @@ -2136,7 +2184,7 @@ def enum_name(EnumClass: type) -> Callable[[int], str]:
>>> import os
>>> import pyrtl
>>> import enum
>>> os.environ["PYRTL_RENDERER"] = "cp437"
>>> os.environ["PYRTL_RENDERER"] = "utf-8-basic"
>>> pyrtl.reset_working_block()

Use ``enum_name`` as a ``repr_func`` or ``repr_per_name`` for
Expand Down
16 changes: 9 additions & 7 deletions pyrtl/wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -1817,9 +1817,7 @@ def __init__(

.. doctest only::

>>> import os
>>> import pyrtl
>>> os.environ["PYRTL_RENDERER"] = "cp437"
>>> pyrtl.reset_working_block()

See :class:`Register`'s documentation above for a basic example. The example
Expand All @@ -1845,13 +1843,17 @@ def __init__(
>>> state.next <<= state + 1

When a ``Register`` is constructed with ``State``,
:meth:`~.SimulationTrace.render_trace` displays ``State`` names by default::
:meth:`~.SimulationTrace.render_trace` displays ``State`` names by default, for
example::

sim = pyrtl.Simulation()
sim.step_multiple(nsteps=4)
sim.tracer.render_trace()

will display::

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=4)
>>> sim.tracer.render_trace()
│0 │1 │2 │3
<BLANKLINE>

state ONE │TWO │THREE│ZERO

:param bitwidth: Number of bits to represent this ``Register``.
Expand Down
10 changes: 7 additions & 3 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import glob
import os
import subprocess
from pathlib import Path

import pytest

Expand All @@ -15,11 +15,15 @@


@pytest.mark.parametrize(
"file", glob.iglob(os.path.dirname(__file__) + "/../examples/*.py")
"file",
(Path(os.path.dirname(__file__)) / ".." / "examples").glob("*.py"),
ids=lambda path: path.name,
)
def test_all_examples(file):
# Always use the ASCII renderer for deterministic example output.
os.environ["PYRTL_RENDERER"] = "ascii"
pyrtl.reset_working_block()
try:
subprocess.check_output(["python", file])
subprocess.check_output(["uv", "run", file])
except subprocess.CalledProcessError as e:
raise e
5 changes: 5 additions & 0 deletions tests/test_simulation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import doctest
import enum
import io
import sys
import unittest

import pyrtl
Expand All @@ -11,6 +12,10 @@ class TestDocTests(unittest.TestCase):
"""Test documentation examples."""

def test_doctests(self):
if sys.stdout.encoding != "utf-8":
msg = "Simulation doctests require UTF-8 support"
raise unittest.SkipTest(msg)

failures, tests = doctest.testmod(m=pyrtl.simulation)
self.assertGreater(tests, 0)
self.assertEqual(failures, 0)
Expand Down
Loading