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..06ad8aeb 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,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 - - 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:: diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py index b529ca82..63a2d8e8 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,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(), } @@ -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:: @@ -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 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)