From 735b1197b7dd8897d5a18051862a1e8543276d8b Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:08:08 -0700 Subject: [PATCH 1/3] Enable Python test GitHub actions workflow on the `windows-latest` runner and fix several Windows compatibility issues: * Rename the `cp437` renderer constants to `utf-8-basic`. The old constants were actually Unicode, and not actually code page 437, so they weren't working as advertised. This removes code page 437 support, since nobody seems to be using it. * Make `render_trace`'s platform detection code more reliable. * Fix `test_examples.py`'s path handling so it works on Windows. * Don't use `render_trace` in doctests outside of `Simulation`, and disable `Simulation` doctests when Unicode is not supported. `Simulation` doctest examples expect Unicode output for `render_trace`. * Fix `examples` so they don't require Unicode. * Fix `justfile` to work on Windows. * Add doctests for `Simulation` and `Simulation.step`. --- .github/workflows/python-test.yml | 2 +- ...ng => pyrtl-renderer-demo-utf-8-basic.png} | Bin docs/simtest.rst | 2 +- examples/introduction-to-hardware.py | 2 +- examples/renderer-demo.py | 31 ++++-- ipynb-examples/introduction-to-hardware.ipynb | 2 +- justfile | 3 + pyrtl/helperfuncs.py | 19 ++-- pyrtl/simulation.py | 88 +++++++++++++----- pyrtl/wire.py | 16 ++-- tests/test_examples.py | 10 +- tests/test_simulation.py | 5 + 12 files changed, 128 insertions(+), 52 deletions(-) rename docs/screenshots/{pyrtl-renderer-demo-cp437.png => pyrtl-renderer-demo-utf-8-basic.png} (100%) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index c36a1f0a..28738c1d 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -8,7 +8,7 @@ jobs: test: strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/docs/screenshots/pyrtl-renderer-demo-cp437.png b/docs/screenshots/pyrtl-renderer-demo-utf-8-basic.png similarity index 100% rename from docs/screenshots/pyrtl-renderer-demo-cp437.png rename to docs/screenshots/pyrtl-renderer-demo-utf-8-basic.png diff --git a/docs/simtest.rst b/docs/simtest.rst index e35119c6..dcb309ef 100644 --- a/docs/simtest.rst +++ b/docs/simtest.rst @@ -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: diff --git a/examples/introduction-to-hardware.py b/examples/introduction-to-hardware.py index 5ff5bf5a..8ca3d11f 100644 --- a/examples/introduction-to-hardware.py +++ b/examples/introduction-to-hardware.py @@ -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)) diff --git a/examples/renderer-demo.py b/examples/renderer-demo.py index c557809c..ab66485b 100644 --- a/examples/renderer-demo.py +++ b/examples/renderer-demo.py @@ -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 @@ -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") diff --git a/ipynb-examples/introduction-to-hardware.ipynb b/ipynb-examples/introduction-to-hardware.ipynb index 2fdef572..3b538dbd 100644 --- a/ipynb-examples/introduction-to-hardware.ipynb +++ b/ipynb-examples/introduction-to-hardware.ipynb @@ -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" ] diff --git a/justfile b/justfile index d5814a1c..e41e7e93 100644 --- a/justfile +++ b/justfile @@ -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: diff --git a/pyrtl/helperfuncs.py b/pyrtl/helperfuncs.py index cbbe270c..d9cf40e6 100644 --- a/pyrtl/helperfuncs.py +++ b/pyrtl/helperfuncs.py @@ -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. @@ -767,15 +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 + bitwidth = 3 + counter = pyrtl.Register(name="counter", bitwidth=bitwidth) + counter.next <<= counter + 1 + + sim = pyrtl.Simulation() + sim.step_multiple(nsteps=2 ** 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 - + counter ──┤1 │2 │3 │-4│-3│-2│-1 :func:`infer_val_and_bitwidth` performs the opposite conversion:: diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py index b529ca82..acfc979b 100644 --- a/pyrtl/simulation.py +++ b/pyrtl/simulation.py @@ -6,6 +6,7 @@ import numbers import os import re +import sys import warnings from collections.abc import Callable, Mapping from dataclasses import dataclass @@ -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 + + a 0x1│0x3 + + b 0x2│0x4 + + sum 0x3│0x7 """ tracer: SimulationTrace @@ -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 @@ -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` 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` to their values for this step. @@ -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 @@ -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` are rendered as square waveforms, with vertical rising and falling edges. Multi-bit :class:`WireVector` values are rendered between vertical bars. - `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 `_, 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 = "│" @@ -1501,18 +1542,23 @@ 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" renderer_map = { "powerline": PowerlineRendererConstants(), "utf-8": Utf8RendererConstants(), "utf-8-alt": Utf8AltRendererConstants(), - "cp437": Cp437RendererConstants(), + "utf-8-basic": Utf8BasicRendererConstants(), "ascii": AsciiRendererConstants(), } @@ -1851,7 +1897,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:: @@ -2136,7 +2182,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 diff --git a/pyrtl/wire.py b/pyrtl/wire.py index 6abef0c1..26b269eb 100644 --- a/pyrtl/wire.py +++ b/pyrtl/wire.py @@ -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 @@ -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 - + state ONE │TWO │THREE│ZERO :param bitwidth: Number of bits to represent this ``Register``. diff --git a/tests/test_examples.py b/tests/test_examples.py index 0a741ed0..b2e930a2 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,6 +1,6 @@ -import glob import os import subprocess +from pathlib import Path import pytest @@ -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 diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 5220e066..ec8b412a 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -1,6 +1,7 @@ import doctest import enum import io +import sys import unittest import pyrtl @@ -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) From c665654b3cf18e8fe96338541610eb87f8d9e8ec Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:54:00 -0700 Subject: [PATCH 2/3] Update `val_to_signed_integer` example so it does proper signed addition. --- pyrtl/helperfuncs.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyrtl/helperfuncs.py b/pyrtl/helperfuncs.py index d9cf40e6..06ad8aeb 100644 --- a/pyrtl/helperfuncs.py +++ b/pyrtl/helperfuncs.py @@ -765,19 +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 ** bitwidth) + sim.step_multiple(nsteps=2 ** counter.bitwidth) sim.tracer.render_trace(repr_func=val_to_signed_integer) will display:: │0 │1 │2 │3 │4 │5 │6 │7 - 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:: From a2e8b72559d87914d6f6c23e9e7831fb114579b8 Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:10:43 -0700 Subject: [PATCH 3/3] Allow `PYRTL_RENDERER=cp437` as a backwards compatibility alias for `PYRTL_RENDERER=utf-8-basic`. --- pyrtl/simulation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py index acfc979b..63a2d8e8 100644 --- a/pyrtl/simulation.py +++ b/pyrtl/simulation.py @@ -1554,11 +1554,13 @@ def default_renderer() -> WaveRenderer: ) renderer = "ascii" + utf_8_basic = Utf8BasicRendererConstants() renderer_map = { "powerline": PowerlineRendererConstants(), "utf-8": Utf8RendererConstants(), "utf-8-alt": Utf8AltRendererConstants(), - "utf-8-basic": Utf8BasicRendererConstants(), + "utf-8-basic": utf_8_basic, + "cp437": utf_8_basic, # For backwards compatibility. "ascii": AsciiRendererConstants(), }