diff --git a/docs/basic.rst b/docs/basic.rst index 04dd40d2..fcc0f2a9 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -122,9 +122,11 @@ maps from :class:`.WireVector` to its default value for the :data:`.conditional_assignment` block. ``defaults`` are not supported for :class:`.MemBlock`. See :ref:`conditional_assignment_defaults` for more details. -See `the state machine example -`_ -for more examples of :data:`.conditional_assignment`. +.. note:: + + See `example3-statemachine + `_ + for more :data:`.conditional_assignment` examples. .. autodata:: pyrtl.otherwise diff --git a/docs/helpers.rst b/docs/helpers.rst index cce16d48..ab2bae6d 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -23,11 +23,10 @@ many operators such as addition and multiplication). Coercion to WireVector ---------------------- -In PyRTL there is only one function in charge of coercing values into -:class:`WireVectors<.WireVector>`, and that is :func:`.as_wires`. This function -is called in almost all helper functions and classes to manage the mixture of -constants and :class:`WireVectors<.WireVector>` that naturally occur in -hardware development. +:func:`.as_wires` coerces values to :class:`WireVectors<.WireVector>`. Most +PyRTL helper functions and classes call :func:`.as_wires` on their inputs, to +manage the mixture of constants and :class:`WireVectors<.WireVector>` that +naturally occur in hardware development. See :ref:`wirevector_coercion` for examples and more details. diff --git a/docs/index.rst b/docs/index.rst index 90f4947f..f17cad47 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,12 +23,10 @@ Quick links Installation ============ -**Automatic installation**:: +PyRTL is availble in `PyPI `_ and can be +installed with :program:`pip`:: - pip install pyrtl - -PyRTL is listed in `PyPI `_ and can be -installed with :program:`pip`. + $ pip install pyrtl Design, Simulate, and Inspect in 15 lines ========================================= diff --git a/docs/rtllib.rst b/docs/rtllib.rst index 6b8204f7..c684a6e3 100644 --- a/docs/rtllib.rst +++ b/docs/rtllib.rst @@ -16,6 +16,7 @@ Adders .. automodule:: pyrtl.rtllib.adders :members: :undoc-members: + :exclude-members: half_adder, one_bit_add, ripple_add, ripple_half_add Multipliers ----------- @@ -55,10 +56,3 @@ AES-128 .. autoclass:: pyrtl.rtllib.aes.AES :members: - -Testing Utilities ------------------ - -.. automodule:: pyrtl.rtllib.testingutils - :members: - :exclude-members: generate_in_wire_and_values, sim_and_ret_out, sim_and_ret_outws, sim_multicycle, multi_sim_multicycle diff --git a/docs/screenshots/render_trace.png b/docs/screenshots/render_trace.png deleted file mode 100644 index 347734fa..00000000 Binary files a/docs/screenshots/render_trace.png and /dev/null differ diff --git a/pyrtl/analysis.py b/pyrtl/analysis.py index 672f2001..fa9ba38c 100644 --- a/pyrtl/analysis.py +++ b/pyrtl/analysis.py @@ -344,6 +344,11 @@ def yosys_area_delay( """Synthesize with `Yosys `_ and return estimate of area and delay. + .. warning:: + + ``yosys_area_delay`` requires ``yosys``, which must be `separately installed + `_. + If ``leave_in_dir`` is specified, that directory will be used to create any temporary files, and the resulting files will be left behind there (which can be useful for manual exploration or debugging) diff --git a/pyrtl/core.py b/pyrtl/core.py index fbc40c01..099e62d6 100644 --- a/pyrtl/core.py +++ b/pyrtl/core.py @@ -1182,6 +1182,12 @@ def set_debug_mode(debug: bool = True): These call stacks can be inspected as :attr:`WireVector.init_call_stack`, and they will appear in :meth:`Block.sanity_check` error messages. + .. note:: + + See `example4-debuggingtools + `_ + for ``debug_mode`` examples. + :param debug: Optional boolean parameter to which the debug mode will be set. """ global debug_mode diff --git a/pyrtl/corecircuits.py b/pyrtl/corecircuits.py index 1da8eee3..cdb156e7 100644 --- a/pyrtl/corecircuits.py +++ b/pyrtl/corecircuits.py @@ -717,7 +717,7 @@ def match_bitwidth(*args: WireVector, signed: bool = False) -> tuple[WireVector] >>> a = pyrtl.Const(-1, name="a_short", signed=True, bitwidth=2) >>> b = pyrtl.Const(-3, name="b", signed=True, bitwidth=4) - >>> a, b = match_bitwidth(a, b, signed=True) + >>> a, b = pyrtl.match_bitwidth(a, b, signed=True) >>> a.name = "a_long" >>> a.bitwidth, b.bitwidth (4, 4) @@ -768,8 +768,8 @@ def as_wires( :class:`Const` :class:`WireVector`). See :ref:`wirevector_coercion`. An example:: >>> def make_my_hardware(a, b): - ... a = as_wires(a) - ... b = as_wires(b) + ... a = pyrtl.as_wires(a) + ... b = pyrtl.as_wires(b) ... return (a + b) & 0xf >>> input = pyrtl.Input(name="input", bitwidth=8) diff --git a/pyrtl/helperfuncs.py b/pyrtl/helperfuncs.py index 1bc5cd6d..cbbe270c 100644 --- a/pyrtl/helperfuncs.py +++ b/pyrtl/helperfuncs.py @@ -50,22 +50,28 @@ def probe(w: WireVector, name: str | None = None) -> WireVector: could be rewritten as:: - y <<= probe(x)[0:3] + 4 + y <<= pyrtl.probe(x)[0:3] + 4 to give visibility into both the origin of ``x`` (including the line that :class:`WireVector` was originally created) and the run-time values of ``x`` (which will be named and thus show up by default in a trace). Likewise:: - y <<= probe(x[0:3]) + 4 - y <<= probe(x[0:3] + 4) - probe(y) <<= x[0:3] + 4 + y <<= pyrtl.probe(x[0:3]) + 4 + y <<= pyrtl.probe(x[0:3] + 4) + pyrtl.probe(y) <<= x[0:3] + 4 are all valid uses of ``probe``. .. note:: - ``probe`` actually adds an :class:`Output` wire to the :ref:`working_block` of - ``w``, which can confuse various post-processing transforms such as + See `example4-debuggingtools + `_ + for more ``probe`` examples. + + .. note:: + + ``probe`` actually adds an :class:`Output` wire to the :ref:`working_block`, + which can confuse various post-processing transforms such as :func:`output_to_verilog`. :param w: :class:`WireVector` from which to get info @@ -184,13 +190,18 @@ def log2(integer_val: int) -> int: Useful when checking that powers of 2 are provided as function inputs. + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + Examples:: - >>> log2(2) + >>> pyrtl.log2(2) 1 - >>> log2(256) + >>> pyrtl.log2(256) 8 - >>> log2(100) + >>> pyrtl.log2(100) Traceback (most recent call last): ... pyrtl.pyrtlexceptions.PyrtlError: this function can only take even powers of 2 @@ -233,20 +244,20 @@ def truncate( Examples:: - >>> truncate(0b101_001, bitwidth=3) + >>> pyrtl.truncate(0b101_001, bitwidth=3) 1 - >>> bin(truncate(0b111_101, bitwidth=3)) + >>> bin(pyrtl.truncate(0b111_101, bitwidth=3)) '0b101' >>> # -1 is 0b1111111... with the number of 1-bits equal to the bitwidth. Python >>> # ints are arbitrary-precision, so this can produce any number of 1-bits. - >>> bin(truncate(-1, bitwidth=3)) + >>> bin(pyrtl.truncate(-1, bitwidth=3)) '0b111' >>> input = pyrtl.Input(name="input", bitwidth=8) - >>> output = truncate(input, bitwidth=4) + >>> output = pyrtl.truncate(input, bitwidth=4) >>> output.name = "output" >>> output.bitwidth 4 @@ -740,31 +751,37 @@ def wirevector_list( def val_to_signed_integer(value: int, bitwidth: int) -> int: """Return ``value`` interpreted as a two's complement signed integer. + .. 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. This is useful for printing and interpreting two's complement values:: - >>> val_to_signed_integer(0xff, bitwidth=8) + >>> pyrtl.val_to_signed_integer(0xff, bitwidth=8) -1 ``val_to_signed_integer`` can also be used as an ``repr_func`` for - :meth:`SimulationTrace.render_trace`, to display signed integers in traces:: + :meth:`~SimulationTrace.render_trace`, to display signed integers in traces:: - bitwidth = 3 - counter = Register(name='counter', bitwidth=bitwidth) - counter.next <<= counter + 1 - sim = Simulation() - sim.step_multiple(nsteps=2 ** bitwidth) + >>> bitwidth = 3 + >>> counter = pyrtl.Register(name="counter", bitwidth=bitwidth) + >>> counter.next <<= counter + 1 - # Generates a trace like: - # │0 │1 │2 │3 │4 │5 │6 │7 - # - # counter ──┤1 │2 │3 │-4│-3│-2│-1 - sim.tracer.render_trace(repr_func=val_to_signed_integer) + >>> 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:: - >>> integer = val_to_signed_integer(0xff, bitwidth=8) - >>> hex(infer_val_and_bitwidth(integer, bitwidth=8).value) + >>> integer = pyrtl.val_to_signed_integer(0xff, bitwidth=8) + >>> hex(pyrtl.infer_val_and_bitwidth(integer, bitwidth=8).value) '0xff' :param value: A Python integer holding the value to convert. @@ -791,32 +808,37 @@ def val_to_signed_integer(value: int, bitwidth: int) -> int: def formatted_str_to_val(data: str, format: str, enum_set=None) -> int: """Return an unsigned integer representation of ``data`` in a specified ``format``. + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + Given a string (not a :class:`WireVector`!) convert that to an unsigned integer ready for input to the simulation environment. This helps deal with signed/unsigned numbers (simulation assumes the values have been converted via two's complement already), but it also takes hex, binary, and enum types as inputs. It is easiest to see how it works with some examples:: - >>> formatted_str_to_val('2', 's3') + >>> pyrtl.formatted_str_to_val('2', 's3') 2 - >>> bin(formatted_str_to_val('-1', 's3')) + >>> bin(pyrtl.formatted_str_to_val('-1', 's3')) '0b111' - >>> bin(formatted_str_to_val('101', 'b3')) + >>> bin(pyrtl.formatted_str_to_val('101', 'b3')) '0b101' - >>> formatted_str_to_val('5', 'u3') + >>> pyrtl.formatted_str_to_val('5', 'u3') 5 - >>> bin(formatted_str_to_val('-3', 's3')) + >>> bin(pyrtl.formatted_str_to_val('-3', 's3')) '0b101' - >>> formatted_str_to_val('a', 'x3') + >>> pyrtl.formatted_str_to_val('a', 'x3') 10 >>> from enum import IntEnum >>> class Ctl(IntEnum): ... ADD = 5 ... SUB = 12 - >>> formatted_str_to_val('ADD', 'e3/Ctl', [Ctl]) + >>> pyrtl.formatted_str_to_val('ADD', 'e3/Ctl', [Ctl]) 5 - >>> formatted_str_to_val('SUB', 'e3/Ctl', [Ctl]) + >>> pyrtl.formatted_str_to_val('SUB', 'e3/Ctl', [Ctl]) 12 :func:`val_to_formatted_str` performs the opposite conversion. @@ -859,32 +881,37 @@ def formatted_str_to_val(data: str, format: str, enum_set=None) -> int: def val_to_formatted_str(val: int, format: str, enum_set=None) -> str: """Return a string representation of the value given format specified. + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + Given an unsigned integer (not a :class:`WireVector`!) convert that to a human-readable string. This helps deal with signed/unsigned numbers (simulation operates on values that have been converted via two's complement), but it also generates hex, binary, and enum types as outputs. It is easiest to see how it works with some examples:: - >>> val_to_formatted_str(2, 's3') + >>> pyrtl.val_to_formatted_str(2, 's3') '2' - >>> val_to_formatted_str(7, 's3') + >>> pyrtl.val_to_formatted_str(7, 's3') '-1' - >>> val_to_formatted_str(5, 'b3') + >>> pyrtl.val_to_formatted_str(5, 'b3') '101' - >>> val_to_formatted_str(5, 'u3') + >>> pyrtl.val_to_formatted_str(5, 'u3') '5' - >>> val_to_formatted_str(5, 's3') + >>> pyrtl.val_to_formatted_str(5, 's3') '-3' - >>> val_to_formatted_str(10, 'x3') + >>> pyrtl.val_to_formatted_str(10, 'x3') 'a' >>> from enum import IntEnum >>> class Ctl(IntEnum): ... ADD = 5 ... SUB = 12 - >>> val_to_formatted_str(5, 'e3/Ctl', [Ctl]) + >>> pyrtl.val_to_formatted_str(5, 'e3/Ctl', [Ctl]) 'ADD' - >>> val_to_formatted_str(12, 'e3/Ctl', [Ctl]) + >>> pyrtl.val_to_formatted_str(12, 'e3/Ctl', [Ctl]) 'SUB' :func:`formatted_str_to_val` performs the opposite conversion. @@ -934,6 +961,11 @@ def infer_val_and_bitwidth( ) -> ValueBitwidthTuple: """Return a ``(value, bitwidth)`` :class:`tuple` inferred from the specified input. + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + Given a boolean, integer, or Verilog-style string constant, this function returns a :class:`ValueBitwidthTuple` ``(value, bitwidth)`` which are inferred from the specified ``rawinput``. If ``signed`` is ``True``, bits will be included to ensure a @@ -941,30 +973,30 @@ def infer_val_and_bitwidth( unsigned representation. Error checks are performed that determine if the bitwidths specified are sufficient and appropriate for the values specified. Examples:: - >>> infer_val_and_bitwidth(2, bitwidth=5) + >>> pyrtl.infer_val_and_bitwidth(2, bitwidth=5) ValueBitwidthTuple(value=2, bitwidth=5) >>> # Infer bitwidth from value. - >>> infer_val_and_bitwidth(3) + >>> pyrtl.infer_val_and_bitwidth(3) ValueBitwidthTuple(value=3, bitwidth=2) - >>> infer_val_and_bitwidth(3).bitwidth + >>> pyrtl.infer_val_and_bitwidth(3).bitwidth 2 >>> # Signed values need an additional sign bit. - >>> infer_val_and_bitwidth(3, signed=True) + >>> pyrtl.infer_val_and_bitwidth(3, signed=True) ValueBitwidthTuple(value=3, bitwidth=3) - >>> val, bitwidth = infer_val_and_bitwidth(-1, bitwidth=3) + >>> val, bitwidth = pyrtl.infer_val_and_bitwidth(-1, bitwidth=3) >>> (bin(val), bitwidth) ('0b111', 3) - >>> infer_val_and_bitwidth("5'd12") + >>> pyrtl.infer_val_and_bitwidth("5'd12") ValueBitwidthTuple(value=12, bitwidth=5) :func:`val_to_signed_integer` performs the opposite conversion:: - >>> val, bitwidth = infer_val_and_bitwidth(-1, bitwidth=3) - >>> val_to_signed_integer(val, bitwidth) + >>> val, bitwidth = pyrtl.infer_val_and_bitwidth(-1, bitwidth=3) + >>> pyrtl.val_to_signed_integer(val, bitwidth) -1 :param rawinput: a bool, int, or Verilog-style string constant @@ -1479,7 +1511,7 @@ def wire_struct(wire_struct_spec): The example ``Byte`` ``@wire_struct`` can be defined as:: - >>> @wire_struct + >>> @pyrtl.wire_struct ... class Byte: ... high: 4 ... low: 4 @@ -1970,7 +2002,7 @@ def wire_matrix(component_schema, size: int, class_name: str | None = None): An example 32-bit ``Word`` ``wire_matrix``, which represents a group of four bytes, can be defined as:: - >>> Word = wire_matrix(component_schema=8, size=4, class_name="Word") + >>> Word = pyrtl.wire_matrix(component_schema=8, size=4, class_name="Word") .. NOTE:: @@ -2045,8 +2077,10 @@ def wire_matrix(component_schema, size: int, class_name: str | None = None): ``wire_matrix`` can be composed with itself and :func:`wire_struct`. For example, we can define some multi-dimensional byte arrays:: - Array1D = wire_matrix(component_schema=8, size=2, class_name="Array1D") - Array2D = wire_matrix(component_schema=Array1D, size=2, class_name="Array2D") + Array1D = pyrtl.wire_matrix(component_schema=8, size=2, class_name="Array1D") + Array2D = pyrtl.wire_matrix(component_schema=Array1D, + size=2, + class_name="Array2D") Drivers must be specified for all components, but they can be specified at any level. All these examples construct an equivalent ``wire_matrix``:: @@ -2068,7 +2102,7 @@ def wire_matrix(component_schema, size: int, class_name: str | None = None): When ``wire_matrix`` is composed with :func:`wire_struct`, components can be accessed by combining the ``[]`` and ``.`` operators:: - @wire_struct + @pyrtl.wire_struct class Byte: high: 4 low: 4 diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index fe1979e9..c8aa7246 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -123,6 +123,14 @@ def input_from_blif( `_ file or string as input, updating the block appropriately. + .. warning:: + + ``input_from_blif`` requires the `pyparsing + `_ ``pip`` package, which is an optional + PyRTL dependency. Install with:: + + $ pip install pyrtl[blif] + If ``merge_io_vectors`` is ``True``, then given 1-bit :class:`Input` wires ``a[0]`` and ``a[1]``, these wires will be combined into a single 2-bit :class:`Input` wire ``a`` that can be accessed by name ``a`` in the block. Otherwise if @@ -649,8 +657,19 @@ def input_from_verilog( block: Block = None, ): """Read an open Verilog file or string as input via `Yosys - `_ conversion, updating the block. Yosys must be - installed. + `_ conversion, updating the block. + + .. warning:: + + ``input_from_verilog`` requires: + + 1. ``yosys``, which must be `separately installed + `_. + + 2. The `pyparsing `_ ``pip`` package, which + is an optional PyRTL dependency. Install with:: + + $ pip install pyrtl[blif] This function runs Yosys with a script to convert Verilog to BLIF, then calls :func:`input_from_blif` on the converted BLIF file. @@ -659,6 +678,11 @@ def input_from_verilog( to try and produce BLIF yourself. Then you can import BLIF directly via :func:`input_from_blif`. + .. doctest comment:: + + This example is not a `doctest` because it depends on Yosys, which might not be + installed. + Example:: verilog = ''' @@ -1488,7 +1512,7 @@ def output_to_verilog( :attr:`~.Register.reset_value`. When this argument is ``True``, this :class:`Register`:: - Register(name="foo", bitwidth=8, reset_value=4) + pyrtl.Register(name="foo", bitwidth=8, reset_value=4) generates this Verilog: @@ -1917,6 +1941,14 @@ def output_to_firrtl( def input_from_iscas_bench(bench, block: Block = None): """Import an ISCAS .bench file + .. warning:: + + ``input_from_iscas_bench`` requires the `pyparsing + `_ ``pip`` package, which is an optional + PyRTL dependency. Install with:: + + $ pip install pyrtl[blif] + :param bench: an open ISCAS .bench file to read :param block: block to add the imported logic (defaults to current :ref:`working_block`) diff --git a/pyrtl/passes.py b/pyrtl/passes.py index caf8dea4..c69ef944 100644 --- a/pyrtl/passes.py +++ b/pyrtl/passes.py @@ -685,7 +685,7 @@ def synthesize( which is identical in function but uses only single bit gates and excludes many of the more complicated :class:`LogicNet` primitives. The new block should consist *almost* exclusively of ``w``, ``&``, ``\\|``, ``^``, and ``~`` - :attr:`ops`, and sequential elements of :class`Registers`, + :attr:`ops`, and sequential elements of :class:`Registers`, which are one bit as well. The two exceptions are for :class:`Inputs` and :class:`Outputs`, to diff --git a/pyrtl/rtllib/aes.py b/pyrtl/rtllib/aes.py index d7620664..6a9c6d7e 100644 --- a/pyrtl/rtllib/aes.py +++ b/pyrtl/rtllib/aes.py @@ -13,49 +13,49 @@ class AES: - """A class for building a PyRTL AES circuit. - - Currently this class only supports 128 bit AES encryption/decryption. - - Example:: - - import pyrtl - from pyrtl.rtllib.aes import AES - - aes = AES() - plaintext = pyrtl.Input(bitwidth=128, name='aes_plaintext') - key = pyrtl.Input(bitwidth=128, name='aes_key') - aes_ciphertext = pyrtl.Output(bitwidth=128, name='aes_ciphertext') - reset = pyrtl.Input(1, name='reset') - ready = pyrtl.Output(1, name='ready') - - ready_out, aes_cipher = aes.encrypt_state_m(plaintext, key, reset) - ready <<= ready_out - aes_ciphertext <<= aes_cipher - - sim = pyrtl.Simulation() - sim.step ({ - 'aes_plaintext': 0x00112233445566778899aabbccddeeff, - 'aes_key': 0x000102030405060708090a0b0c0d0e0f, - 'reset': 1 - }) - for cycle in range(1,10): - sim.step ({ - 'aes_plaintext': 0x00112233445566778899aabbccddeeff, - 'aes_key': 0x000102030405060708090a0b0c0d0e0f, - 'reset': 0 - }) - sim.tracer.render_trace(symbol_len=40, segment_size=1) + """Builds `AES `_ + encryption and decryption circuits. + + Currently this class only supports 128-bit AES encryption and decryption, with + single-cycle and multi-cycle implementations. """ def __init__(self): self.memories_built = False self._key_len = 128 - def encryption( + def single_cycle_encrypt( self, plaintext: pyrtl.WireVector, key: pyrtl.WireVector ) -> pyrtl.WireVector: - """Builds a single cycle AES Encryption circuit. + """Build a single-cycle AES encryption circuit. + + See also :meth:`~AES.multi_cycle_encrypt`, which builds a multi-cycle AES + encryption circuit. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + Example:: + + >>> plaintext = pyrtl.Input(name="plaintext", bitwidth=128) + >>> key = pyrtl.Input(name="key", bitwidth=128) + >>> ciphertext = pyrtl.Output(name="ciphertext", bitwidth=128) + + >>> aes = pyrtl.rtllib.aes.AES() + >>> ciphertext <<= aes.single_cycle_encrypt(plaintext, key) + + >>> plaintext_value = 0x112233445566778899aabbccddeeff + >>> key_value = 0x102030405060708090a0b0c0d0e0f + + >>> sim = pyrtl.Simulation() + >>> sim.step({"plaintext": plaintext_value, "key": key_value}) + >>> sim.inspect("ciphertext") + 140591190147677442632770771134392354138 + + The ``ciphertext`` produced by this example should match the ``ciphertext`` + produced by the :meth:`~AES.multi_cycle_encrypt` example. :param plaintext: Text to encrypt. :param key: AES key to use to encrypt. @@ -80,13 +80,51 @@ def encryption( t = self._add_round_key(t, key_list[round]) return t - def encrypt_state_m( + def multi_cycle_encrypt( self, plaintext_in: pyrtl.WireVector, key_in: pyrtl.WireVector, reset: pyrtl.WireVector, ) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: - """Builds a multiple cycle AES Encryption state machine circuit. + """Build a multi-cycle AES encryption circuit. + + See also :meth:`~AES.single_cycle_encrypt`, which builds a single-cycle AES + encryption circuit. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + Example:: + + >>> plaintext = pyrtl.Input(name="plaintext", bitwidth=128) + >>> key = pyrtl.Input(name="key", bitwidth=128) + >>> reset = pyrtl.Input(name="reset", bitwidth=1) + >>> ready = pyrtl.Output(name="ready", bitwidth=1) + >>> ciphertext = pyrtl.Output(name="ciphertext", bitwidth=128) + + >>> aes = pyrtl.rtllib.aes.AES() + >>> ready_, ciphertext_ = aes.multi_cycle_encrypt(plaintext, key, reset) + >>> ready <<= ready_ + >>> ciphertext <<= ciphertext_ + + >>> plaintext_value = 0x112233445566778899aabbccddeeff + >>> key_value = 0x102030405060708090a0b0c0d0e0f + + >>> sim = pyrtl.Simulation() + >>> sim.step({ + ... "plaintext": plaintext_value, + ... "key": key_value, + ... "reset": True + ... }) + >>> while not sim.inspect("ready"): + ... sim.step({"plaintext": 0, "key": 0, "reset": False}) + >>> sim.inspect("ciphertext") + 140591190147677442632770771134392354138 + + The ``ciphertext`` produced by this example should match the ``ciphertext`` + produced by the :meth:`~AES.single_cycle_encrypt` example. :param plaintext: Text to encrypt. :param key: AES key to use to encrypt. @@ -138,10 +176,36 @@ def encrypt_state_m( ready = counter == 10 return ready, plain_text - def decryption( + def single_cycle_decrypt( self, ciphertext: pyrtl.WireVector, key: pyrtl.WireVector ) -> pyrtl.WireVector: - """Builds a single cycle AES Decryption circuit. + """Build a single-cycle AES decryption circuit. + + See also :meth:`~AES.multi_cycle_decrypt`, which builds a multi-cycle AES + decryption circuit. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + Example that decrypts the ciphertext from the :meth:`~AES.single_cycle_encrypt` + example:: + + >>> ciphertext = pyrtl.Input(name="ciphertext", bitwidth=128) + >>> key = pyrtl.Input(name="key", bitwidth=128) + >>> plaintext = pyrtl.Output(name="plaintext", bitwidth=128) + + >>> aes = pyrtl.rtllib.aes.AES() + >>> plaintext <<= aes.single_cycle_decrypt(ciphertext, key) + + >>> ciphertext_value = 140591190147677442632770771134392354138 + >>> key_value = 0x102030405060708090a0b0c0d0e0f + + >>> sim = pyrtl.Simulation() + >>> sim.step({"ciphertext": ciphertext_value, "key": key_value}) + >>> print(hex(sim.inspect("plaintext"))) + 0x112233445566778899aabbccddeeff :param ciphertext: Data to decrypt. :param key: AES key to use to encrypt (AES is symmetric). @@ -166,13 +230,49 @@ def decryption( return t - def decryption_statem( + def multi_cycle_decrypt( self, ciphertext_in: pyrtl.WireVector, key_in: pyrtl.WireVector, reset: pyrtl.WireVector, ) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: - """Builds a multiple cycle AES Decryption state machine circuit. + """Build a multi-cycle AES decryption circuit. + + See also :meth:`~AES.single_cycle_decrypt`, which builds a single-cycle AES + decryption circuit. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + Example that decrypts the ciphertext from the :meth:`~AES.single_cycle_encrypt` + example:: + + >>> ciphertext = pyrtl.Input(name="ciphertext", bitwidth=128) + >>> key = pyrtl.Input(name="key", bitwidth=128) + >>> reset = pyrtl.Input(name="reset", bitwidth=1) + >>> ready = pyrtl.Output(name="ready", bitwidth=1) + >>> plaintext = pyrtl.Output(name="plaintext", bitwidth=128) + + >>> aes = pyrtl.rtllib.aes.AES() + >>> ready_, plaintext_ = aes.multi_cycle_decrypt(ciphertext, key, reset) + >>> ready <<= ready_ + >>> plaintext <<= plaintext_ + + >>> ciphertext_value = 140591190147677442632770771134392354138 + >>> key_value = 0x102030405060708090a0b0c0d0e0f + + >>> sim = pyrtl.Simulation() + >>> sim.step({ + ... "ciphertext": ciphertext_value, + ... "key": key_value, + ... "reset": True + ... }) + >>> while not sim.inspect("ready"): + ... sim.step({"ciphertext": 0, "key": 0, "reset": False}) + >>> print(hex(sim.inspect("plaintext"))) + 0x112233445566778899aabbccddeeff :param ciphertext: Data to decrypt. :param key: AES key to use to encrypt (AES is symmetric). @@ -230,6 +330,19 @@ def decryption_statem( ready = counter == 10 return ready, cipher_text + # Aliases for deprecated method names. + def encryption(self, *args, **kwargs): + return self.single_cycle_encrypt(*args, **kwargs) + + def encrypt_state_m(self, *args, **kwargs): + return self.multi_cycle_encrypt(*args, **kwargs) + + def decryption(self, *args, **kwargs): + return self.single_cycle_decrypt(*args, **kwargs) + + def decryption_statem(self, *args, **kwargs): + return self.multi_cycle_decrypt(*args, **kwargs) + def _key_gen(self, key): keys = [key] for enc_round in range(10): diff --git a/pyrtl/rtllib/prngs.py b/pyrtl/rtllib/prngs.py index 447c33be..51395e42 100644 --- a/pyrtl/rtllib/prngs.py +++ b/pyrtl/rtllib/prngs.py @@ -13,41 +13,50 @@ def prng_lfsr( req: pyrtl.WireVector, seed: pyrtl.WireVector = None, ) -> pyrtl.Register: - """Builds a single-cycle PRNG using a 127 bits Fibonacci LFSR. + """Builds a single-cycle PRNG using a 127 bits Fibonacci `LFSR + `_. A very fast and compact PRNG that generates a random number using only one clock cycle. Has a period of ``2**127 - 1``. Its linearity makes it a bit statistically weak, but it should be good enough for any noncryptographic purpose like test pattern generation. - Example:: + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() - load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') - rand = pyrtl.Output(64, 'rand') + Example:: - rand <<= prngs.prng_lfsr(64, load, req) + >>> load = pyrtl.Input(1, "load") + >>> req = pyrtl.Input(1, "req") + >>> seed = pyrtl.Input(127, "seed") + >>> rand = pyrtl.Output(32, "rand") - sim = pyrtl.Simulation() - sim.step({'load': 1, 'req': 0}) # seed once at the beginning - sim.step({'load': 0, 'req': 1}) - sim.step({'load': 0, 'req': 0}) - print(sim.inspect(rand)) - sim.tracer.render_trace(symbol_len=40, segment_size=1) + >>> rand <<= pyrtl.rtllib.prngs.prng_lfsr( + ... bitwidth=32, + ... load=load, + ... req=req, + ... seed=seed + ... ) - Example with explicit seeding:: + >>> seed_value = 0x70123456789012345678901234567890 + >>> seed_value.bit_length() + 127 - seed = pyrtl.Input(127, 'seed') - load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') - rand = pyrtl.Output(32, 'rand') + >>> # Start `Simulation` and load `seed`. + >>> sim = pyrtl.Simulation() + >>> sim.step({"load": True, "req": False, "seed": seed_value}) - rand <<= prngs.prng_lfsr(32, load, req, seed) + >>> # Generate a random number. + >>> sim.step({"load": False, "req": True, "seed": 0}) + >>> sim.inspect("rand") + 878082192 - sim = pyrtl.Simulation() - sim.step({'load': 1, 'req': 0, 'seed': 0x102030405060708090a0b0c0d0e0f010}) - sim.step({'load': 0, 'req': 1, 'seed': 0x102030405060708090a0b0c0d0e0f010}) - sim.step({'load': 0, 'req': 0, 'seed': 0x102030405060708090a0b0c0d0e0f010}) - print(sim.inspect(rand)) - sim.tracer.render_trace(symbol_len=40, segment_size=1) + >>> # Generate a random number. + >>> sim.step({"load": False, "req": True, "seed": 0}) + >>> sim.inspect("rand") + 543996405 :param bitwidth: The desired bitwidth of the random number. :param load: One bit signal to load the seed into the PRNG. @@ -95,23 +104,49 @@ def prng_xoroshiro128( See also http://xoroshiro.di.unimi.it/ - Example:: - - load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') - ready, rand = pyrtl.Output(1, 'ready'), pyrtl.Output(128, 'rand') + .. doctest only:: - ready_out, rand_out = prngs.prng_xoroshiro128(128, load, req) - ready <<= ready_out - rand <<= rand_out + >>> import pyrtl + >>> pyrtl.reset_working_block() - sim = pyrtl.Simulation() - sim.step({'load': 1, 'req': 0}) # seed once at the beginning - sim.step({'load': 0, 'req': 1}) - while sim.value[ready] == 0: # or loop 2 cycles - sim.step({'load': 0, 'req': 0}) + Example:: - print(sim.inspect(rand)) - sim.tracer.render_trace(symbol_len=40, segment_size=1) + >>> load = pyrtl.Input(1, "load") + >>> req = pyrtl.Input(1, "req") + >>> seed = pyrtl.Input(128, "seed") + >>> ready = pyrtl.Output(1, "ready") + >>> rand = pyrtl.Output(128, "rand") + + >>> ready_, rand_ = pyrtl.rtllib.prngs.prng_xoroshiro128( + ... bitwidth=128, + ... load=load, + ... req=req, + ... seed=seed + ... ) + >>> ready <<= ready_ + >>> rand <<= rand_ + + >>> seed_value = 0x90123456789012345678901234567890 + >>> seed_value.bit_length() + 128 + + >>> # Start `Simulation` and load `seed`. + >>> sim = pyrtl.Simulation() + >>> sim.step({"load": True, "req": False, "seed": seed_value}) + + >>> # Generate a random number. + >>> sim.step({"load": False, "req": True, "seed": 0}) + >>> while not sim.inspect("ready"): + ... sim.step({"load": False, "req": False, "seed": 0}) + >>> sim.inspect("rand") + 306442959642529804138987790876643657180 + + >>> # Generate a random number. + >>> sim.step({"load": False, "req": True, "seed": 0}) + >>> while not sim.inspect("ready"): + ... sim.step({"load": False, "req": False, "seed": 0}) + >>> sim.inspect("rand") + 102902340102369319506108813713726960781 :param bitwidth: The desired bitwidth of the random number. :param load: One bit signal to load the seed into the PRNG. @@ -145,12 +180,13 @@ def prng_xoroshiro128( rand = pyrtl.Register(gen_cycles * 64) counter = pyrtl.Register(counter_bitwidth, "counter") gen_done = counter == gen_cycles - 1 - state = pyrtl.Register(1) class State(enum.IntEnum): WAIT = 0 GEN = 1 + state = pyrtl.Register(State=State) + with pyrtl.conditional_assignment: with load: s0.next |= seed[:64] @@ -196,27 +232,51 @@ def csprng_trivium( See also the eSTREAM portfolio page: http://www.ecrypt.eu.org/stream/e2-trivium.html - Example:: - - load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') - ready, rand = pyrtl.Output(1, 'ready'), pyrtl.Output(128, 'rand') + .. doctest only:: - ready_out, rand_out = prngs.csprng_trivium(128, load, req) - ready <<= ready_out - rand <<= rand_out + >>> import pyrtl + >>> pyrtl.reset_working_block() - sim = pyrtl.Simulation() - # Seed only in the first cycle. - sim.step({'load': 1, 'req': 0}) - while sim.value[ready] == 0: # or loop 19 cycles - sim.step({'load': 0, 'req': 0}) - - sim.step({'load': 0, 'req': 1}) - while sim.value[ready] == 0: # or loop 2 cycles - sim.step({'load': 0, 'req': 0}) + Example:: - print(sim.inspect(rand)) - sim.tracer.render_trace(symbol_len=45, segment_size=5) + >>> load = pyrtl.Input(1, "load") + >>> req = pyrtl.Input(1, "req") + >>> seed = pyrtl.Input(160, "seed") + >>> ready = pyrtl.Output(1, "ready") + >>> rand = pyrtl.Output(128, "rand") + + >>> ready_, rand_ = pyrtl.rtllib.prngs.csprng_trivium( + ... bitwidth=128, + ... load=load, + ... req=req, + ... seed=seed + ... ) + >>> ready <<= ready_ + >>> rand <<= rand_ + + >>> seed_value = 0x8234567890123456789012345678901234567890 + >>> seed_value.bit_length() + 160 + + >>> # Start `Simulation` and load `seed`. + >>> sim = pyrtl.Simulation() + >>> sim.step({"load": True, "req": False, "seed": seed_value}) + >>> while not sim.inspect("ready"): + ... sim.step({"load": False, "req": False, "seed": 0}) + + >>> # Generate a random number. + >>> sim.step({"load": False, "req": True, "seed": 0}) + >>> while not sim.inspect("ready"): + ... sim.step({"load": False, "req": False, "seed": 0}) + >>> sim.inspect("rand") + 204619253771835913275124539130029978896 + + >>> # Generate a random number. + >>> sim.step({"load": False, "req": True, "seed": 0}) + >>> while not sim.inspect("ready"): + ... sim.step({"load": False, "req": False, "seed": 0}) + >>> sim.inspect("rand") + 98933539811155255006597373162055402029 :param bitwidth: The desired bitwidth of the random number. :param load: One bit signal to load the seed into the PRNG @@ -267,13 +327,14 @@ def csprng_trivium( counter = pyrtl.Register(counter_bitwidth, "counter") init_done = counter == init_cycles gen_done = counter == gen_cycles - 1 - state = pyrtl.Register(2) class State(enum.IntEnum): WAIT = 0 INIT = 1 GEN = 2 + state = pyrtl.Register(State=State) + with pyrtl.conditional_assignment: with load: counter.next |= 0 diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py index 54659028..fefcaab7 100644 --- a/pyrtl/simulation.py +++ b/pyrtl/simulation.py @@ -74,12 +74,26 @@ class Simulation: """ Stores the simulation results for each cycle. + .. doctest only:: + + >>> import os + >>> import pyrtl + >>> os.environ["PYRTL_RENDERER"] = "cp437" + >>> pyrtl.reset_working_block() + ``tracer`` is typically used to render simulation waveforms with :meth:`~SimulationTrace.render_trace`, for example:: - sim = pyrtl.Simulation() - sim.step_multiple(nsteps=10) - sim.tracer.render_trace() + >>> counter = pyrtl.Register(name="counter", bitwidth=2) + >>> counter.next <<= counter + 1 + + >>> sim = pyrtl.Simulation() + >>> sim.step_multiple(nsteps=8) + + >>> sim.tracer.render_trace() + │0 │1 │2 │3 │4 │5 │6 │7 + + counter ───┤0x1│0x2│0x3├───┤0x1│0x2│0x3 See :class:`SimulationTrace` for more display options. """ @@ -1514,9 +1528,6 @@ def __getitem__(self, key): return self.__data[key] -_default_renderer = default_renderer() - - class SimulationTrace: """Storage and presentation of simulation waveforms. @@ -1788,7 +1799,7 @@ def render_trace( self, trace_list: list[str] | None = None, file: TextIO | None = None, - renderer: WaveRenderer = _default_renderer, + renderer: WaveRenderer | None = None, symbol_len: int | None = None, repr_func: Callable[[int], str] = hex, repr_per_name: dict[str, Callable[[int], str]] | None = None, @@ -1796,20 +1807,25 @@ def render_trace( ): """Render the trace with Unicode and ASCII escape sequences. - The resulting output can be viewed directly in a terminal. Example:: + .. doctest only:: - counter = pyrtl.Register(name="counter", bitwidth=2) - counter.next <<= counter + 1 + >>> import os + >>> import pyrtl + >>> os.environ["PYRTL_RENDERER"] = "cp437" + >>> pyrtl.reset_working_block() - sim = pyrtl.Simulation() - sim.step_multiple(nsteps=4) + The resulting output can be viewed directly in a terminal. Example:: - sim.tracer.render_trace() + >>> counter = pyrtl.Register(name="counter", bitwidth=2) + >>> counter.next <<= counter + 1 - Which displays: + >>> sim = pyrtl.Simulation() + >>> sim.step_multiple(nsteps=8) - .. image:: ../docs/screenshots/render_trace.png - :scale: 66% + >>> sim.tracer.render_trace() + │0 │1 │2 │3 │4 │5 │6 │7 + + counter ───┤0x1│0x2│0x3├───┤0x1│0x2│0x3 Many trace formats are available, and can be configured with the ``PYRTL_RENDERER`` environment variable. See :class:`.WaveRenderer`'s @@ -1839,6 +1855,8 @@ def render_trace( if len(self) == 0: msg = "You need to step the simulation at least once to render a trace." raise PyrtlError(msg) + if renderer is None: + renderer = default_renderer() if repr_per_name is None: repr_per_name = {} if _currently_in_jupyter_notebook(): @@ -1983,16 +2001,19 @@ def formatted_trace_line(wire, trace): maxtracelen = max(len(self.trace[trace_name]) for trace_name in trace_list) if segment_size is None: segment_size = maxtracelen - spaces = " " * (maxnamelen) ticks = [ renderer.render_ruler_segment(n, cycle_len, segment_size, maxtracelen) for n in range(0, maxtracelen, segment_size) ] - print(spaces + "".join(ticks), file=file) + ruler_line = " " * (maxnamelen) + "".join(ticks) + print(ruler_line.rstrip(), file=file) # now all the traces for trace_name in trace_list: - print(formatted_trace_line(trace_name, self.trace[trace_name]), file=file) + print( + formatted_trace_line(trace_name, self.trace[trace_name]).rstrip(), + file=file, + ) def _set_initial_values( self, @@ -2072,8 +2093,10 @@ def enum_name(EnumClass: type) -> Callable[[int], str]: .. doctest only:: + >>> import os >>> import pyrtl >>> import enum + >>> os.environ["PYRTL_RENDERER"] = "cp437" >>> pyrtl.reset_working_block() Use ``enum_name`` as a ``repr_func`` or ``repr_per_name`` for @@ -2088,16 +2111,13 @@ def enum_name(EnumClass: type) -> Callable[[int], str]: :meth:`~SimulationTrace.render_trace` example:: - option = pyrtl.Input(name="option", bitwidth=1) - - sim = pyrtl.Simulation() - sim.step_multiple({"option": [Option.FOO, Option.BAR]}) - sim.tracer.render_trace(repr_per_name={"option": pyrtl.enum_name(Option)}) - - Which prints:: + >>> option = pyrtl.Input(name="option", bitwidth=1) + >>> sim = pyrtl.Simulation() + >>> sim.step_multiple({"option": [Option.FOO, Option.BAR]}) + >>> sim.tracer.render_trace(repr_per_name={"option": pyrtl.enum_name(Option)}) │0 │1 - + option FOO│BAR .. note:: diff --git a/pyrtl/visualization.py b/pyrtl/visualization.py index 0cc59e1f..96e1b27a 100644 --- a/pyrtl/visualization.py +++ b/pyrtl/visualization.py @@ -581,8 +581,14 @@ def output_to_svg(file: TextIO, *args, **kwargs): def block_to_svg(*args, **kwargs) -> str: - """Return a SVG rendering of a ``block``. Requires the `graphviz - `_ ``pip`` package. + """Return a SVG rendering of a ``block``. + + .. warning:: + + ``block_to_svg`` requires the `graphviz `_ + ``pip`` package, which is an optional PyRTL dependency. Install with:: + + $ pip install pyrtl[svg] .. doctest only:: @@ -628,7 +634,7 @@ def block_to_svg(*args, **kwargs) -> str: # py-graphviz 0.19 or later return svg except ImportError as exc: - msg = 'need graphviz installed (try "pip install graphviz")' + msg = 'need graphviz installed (try "pip install pyrtl[svg]")' raise PyrtlError(msg) from exc diff --git a/pyrtl/wire.py b/pyrtl/wire.py index a0e2e424..6abef0c1 100644 --- a/pyrtl/wire.py +++ b/pyrtl/wire.py @@ -271,7 +271,7 @@ class WireVector: """ block: Block - """The ``Block`` that this :class:`WireVector` belongs to.""" + """The :class:`.Block` that this :class:`WireVector` belongs to.""" # "code" is a static variable used when output as string. # Each class inheriting from WireVector should overload accordingly @@ -1817,7 +1817,9 @@ 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 +1847,11 @@ def __init__( When a ``Register`` is constructed with ``State``, :meth:`~.SimulationTrace.render_trace` displays ``State`` names by default:: - sim = pyrtl.Simulation() - sim.step_multiple(nsteps=4) - sim.tracer.render_trace() - - Which prints:: - + >>> 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/rtllib/test_aes.py b/tests/rtllib/test_aes.py index e196c09a..356fe4f4 100644 --- a/tests/rtllib/test_aes.py +++ b/tests/rtllib/test_aes.py @@ -1,3 +1,4 @@ +import doctest import unittest import pyrtl @@ -5,6 +6,15 @@ from pyrtl.rtllib import aes, testingutils +class TestDocTests(unittest.TestCase): + """Test documentation examples.""" + + def test_aes_doctests(self): + failures, tests = doctest.testmod(m=aes) + self.assertGreater(tests, 0) + self.assertEqual(failures, 0) + + class TestAESDecrypt(unittest.TestCase): """ Test vectors are retrieved from: diff --git a/tests/rtllib/test_prngs.py b/tests/rtllib/test_prngs.py index ba440155..426cb062 100644 --- a/tests/rtllib/test_prngs.py +++ b/tests/rtllib/test_prngs.py @@ -1,3 +1,4 @@ +import doctest import random import unittest from itertools import islice @@ -6,6 +7,15 @@ from pyrtl.rtllib import prngs +class TestDocTests(unittest.TestCase): + """Test documentation examples.""" + + def test_prngs_doctests(self): + failures, tests = doctest.testmod(m=prngs) + self.assertGreater(tests, 0) + self.assertEqual(failures, 0) + + class TestPrngs(unittest.TestCase): def setUp(self): pyrtl.reset_working_block() diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 2205bc79..5220e066 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -173,9 +173,9 @@ def check_rendered_trace(self, expected, **kwargs): def test_hex_trace(self): expected = ( - " |0 |1 |2 |3 |4 \n" + " |0 |1 |2 |3 |4\n" " \n" - "a 0x1 |0x4 |0x9 |0xb |0xc \n" + "a 0x1 |0x4 |0x9 |0xb |0xc\n" " \n" "b 0x2 |0x17|0x2b|0x78|----\n" " \n" @@ -185,9 +185,9 @@ def test_hex_trace(self): def test_oct_trace(self): expected = ( - " |0 |1 |2 |3 |4 \n" + " |0 |1 |2 |3 |4\n" " \n" - "a 0o1 |0o4 |0o11 |0o13 |0o14 \n" + "a 0o1 |0o4 |0o11 |0o13 |0o14\n" " \n" "b 0o2 |0o27 |0o53 |0o170|-----\n" " \n" @@ -198,9 +198,9 @@ def test_oct_trace(self): def test_bin_trace(self): expected = ( - " |0 |1 |2 |3 |4 \n" + " |0 |1 |2 |3 |4\n" " \n" - "a 0b1 |0b100 |0b1001 |0b1011 |0b1100 \n" + "a 0b1 |0b100 |0b1001 |0b1011 |0b1100\n" " \n" "b 0b10 |0b10111 |0b101011 |0b1111000|---------\n" " \n" @@ -211,9 +211,9 @@ def test_bin_trace(self): def test_decimal_trace(self): expected = ( - " |0 |1 |2 |3 |4 \n" + " |0 |1 |2 |3 |4\n" " \n" - "a 1 |4 |9 |11 |12 \n" + "a 1 |4 |9 |11 |12\n" " \n" "b 2 |23 |43 |120|---\n" " \n" @@ -279,7 +279,7 @@ class State(enum.IntEnum): repr_per_name={state.name: state_name}, ) expected = ( - " |0 |1 \n" + " |0 |1\n" " \n" "state FOO|BAR\n" ) # fmt: skip @@ -316,7 +316,7 @@ def test_val_to_signed_integer(self): file=buff, renderer=self.renderer, repr_func=pyrtl.val_to_signed_integer ) expected = ( - " |0 |1 |2 |3 \n" + " |0 |1 |2 |3\n" " \n" "counter --|1 |-2|-1\n" ) # fmt: skip @@ -352,13 +352,13 @@ class Foo(enum.IntEnum): renderer=self.renderer, ) expected = ( - " |0 |1 |2 |3 |4 \n" + " |0 |1 |2 |3 |4\n" " \n" " i 0x1|0x2|0x4|0x8|---\n" " \n" " o -------|0x1|0x2|0x3\n" " \n" - "state A |B |C |D \n" + "state A |B |C |D\n" ) self.assertEqual(buff.getvalue(), expected)