diff --git a/docs/README.md b/docs/README.md index 73f45899..cb03c3a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,8 +13,12 @@ PyRTL's documentation is in this `docs` directory. It is built with The main Sphinx configuration file is [`docs/conf.py`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/conf.py). -Most of PyRTL's documentation is automatically extracted from Python docstrings, see -[docstring formating](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures) for supported directives and fields. +Most of PyRTL's documentation is automatically extracted from Python +docstrings, see [docstring +formating](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures) +for supported directives and fields. Sphinx parses [Python type +annotations](https://docs.python.org/3/library/typing.html), so put type +information into annotations instead of docstrings. Follow the instructions on this page to build a local copy of PyRTL's documentation. This is useful for verifying that PyRTL's documentation still @@ -42,7 +46,7 @@ repository root: ```shell # Install Sphinx. -$ pip install -r docs/requirements.txt +$ pip install --upgrade -r docs/requirements.txt ``` ## Installing Graphviz diff --git a/pyproject.toml b/pyproject.toml index d476bb96..f748ea03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,10 @@ classifiers = [ ] [project.optional-dependencies] +# Required by `input_from_blif`. blif = ["pyparsing"] +# Required by `block_to_svg`. +svg = ["graphviz"] [project.urls] Homepage = "http://ucsbarchlab.github.io/PyRTL/" diff --git a/pyrtl/helperfuncs.py b/pyrtl/helperfuncs.py index 2069e9fe..03b53f60 100644 --- a/pyrtl/helperfuncs.py +++ b/pyrtl/helperfuncs.py @@ -519,19 +519,36 @@ def wirevector_list(names, bitwidth=None, wvtype=WireVector): return wirelist -def val_to_signed_integer(value, bitwidth): - """ Return value as intrepreted as a signed integer under two's complement. +def val_to_signed_integer(value: int, bitwidth: int) -> int: + """Return value as intrepreted as a signed integer under two's complement. - :param int value: a Python integer holding the value to convert - :param int bitwidth: the length of the integer in bits to assume for conversion - :return: `value` as a signed integer - :rtype: int + :param value: A Python integer holding the value to convert. + :param bitwidth: The length of the integer in bits to assume for + conversion. + :return: ``value`` as a signed integer - Given an unsigned integer (not a WireVector!) convert that to a signed + Given an unsigned integer (not a ``WireVector``!) convert that to a signed integer. This is useful for printing and interpreting values which are negative numbers in two's complement. :: val_to_signed_integer(0xff, 8) == -1 + + ``val_to_signed_integer`` can also be used as an ``repr_func`` for + :py: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) + + # 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) + """ if isinstance(value, WireVector) or isinstance(bitwidth, WireVector): raise PyrtlError('inputs must not be wirevectors') diff --git a/pyrtl/rtllib/multipliers.py b/pyrtl/rtllib/multipliers.py index 053ca09c..165dad13 100644 --- a/pyrtl/rtllib/multipliers.py +++ b/pyrtl/rtllib/multipliers.py @@ -170,17 +170,12 @@ def signed_tree_multiplier(A, B, reducer=adders.wallace_reducer, adder_func=adde return _twos_comp_conditional(res, aneg ^ bneg) -def _twos_comp_conditional(orig_wire, sign_bit, bw=None): - """Returns two's complement of wire (using bitwidth bw) if sign_bit == 1""" - if bw is None: - bw = len(orig_wire) - new_wire = pyrtl.WireVector(bw) - with pyrtl.conditional_assignment: - with sign_bit: - new_wire |= ~orig_wire + 1 - with pyrtl.otherwise: - new_wire |= orig_wire - return new_wire +def _twos_comp_conditional(orig_wire: pyrtl.WireVector, + sign_bit: pyrtl.WireVector) -> pyrtl.WireVector: + """Returns two's complement of ``orig_wire`` if ``sign_bit`` == 1""" + return pyrtl.select(sign_bit, + (~orig_wire + 1).truncate(len(orig_wire)), + orig_wire) def fused_multiply_adder(mult_A, mult_B, add, signed=False, reducer=adders.wallace_reducer, diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py index fa949ee9..a766ff05 100644 --- a/pyrtl/simulation.py +++ b/pyrtl/simulation.py @@ -1,5 +1,7 @@ """Classes for executing and tracing circuit simulations.""" +from __future__ import annotations + import copy import math import numbers @@ -13,6 +15,7 @@ from .wire import Input, Register, Const, Output, WireVector from .memory import RomBlock from .helperfuncs import check_rtl_assertions, _currently_in_jupyter_notebook +from .helperfuncs import val_to_signed_integer from .importexport import _VerilogSanitizer try: @@ -63,7 +66,7 @@ class Simulation(object): simple_func = { # OPS 'w': lambda x: x, - '~': lambda x: ~x, + '~': lambda x: ~int(x), '&': lambda left, right: left & right, '|': lambda left, right: left | right, '^': lambda left, right: left ^ right, @@ -1026,11 +1029,12 @@ def render_ruler_segment(self, n, cycle_len, segment_size, maxtracelen): ticks = major_tick.ljust(cycle_len * segment_size) return ticks - def val_to_str(self, value, wire_name, repr_func, repr_per_name): + def val_to_str(self, value: int, wire: WireVector, + repr_func: typing.Callable, repr_per_name: dict) -> str: """Return a string representing 'value'. :param value: The value to convert to string. - :param wire_name: Name of the wire that produced this value. + :param wire: Wire that produced this value. :param repr_func: function to use for representing the current_val; examples are 'hex', 'oct', 'bin', 'str' (for decimal), or the function returned by :py:func:`enum_name`. Defaults to 'hex'. @@ -1041,11 +1045,18 @@ def val_to_str(self, value, wire_name, repr_func, repr_per_name): :return: a string representing 'value'. """ - f = repr_per_name.get(wire_name) + f = repr_per_name.get(wire.name) + + def invoke_f(f, value): + if f is val_to_signed_integer: + return str(val_to_signed_integer(value=value, + bitwidth=wire.bitwidth)) + else: + return str(f(value)) if f is not None: - return str(f(value)) + return invoke_f(f, value) else: - return str(repr_func(value)) + return invoke_f(repr_func, value) def render_val(self, w, prior_val, current_val, symbol_len, cycle_len, repr_func, repr_per_name, prev_line, is_last): @@ -1081,7 +1092,8 @@ def render_val(self, w, prior_val, current_val, symbol_len, cycle_len, flat_zero = (w.name not in repr_per_name and (repr_func is hex or repr_func is oct or repr_func is int or repr_func is str - or repr_func is bin)) + or repr_func is bin + or repr_func is val_to_signed_integer)) if prev_line: # Bus wires are currently never rendered across multiple lines. return '' @@ -1105,7 +1117,7 @@ def render_val(self, w, prior_val, current_val, symbol_len, cycle_len, if prior_val is None: out += self.constants._bus_start # Display the current non-zero value. - out += (self.val_to_str(current_val, w.name, repr_func, + out += (self.val_to_str(current_val, w, repr_func, repr_per_name).rstrip('L') .ljust(symbol_len)[:symbol_len]) if is_last: @@ -1591,28 +1603,36 @@ def print_trace_strs(time): file.flush() def render_trace( - self, trace_list=None, file=sys.stdout, renderer=default_renderer(), - symbol_len=None, repr_func=hex, repr_per_name={}, segment_size=1): + self, trace_list: list[str] = None, file=sys.stdout, + renderer: WaveRenderer = default_renderer(), symbol_len: int = None, + repr_func: typing.Callable = hex, repr_per_name: dict = {}, + segment_size: int = 1): - """ Render the trace to a file using unicode and ASCII escape sequences. + """Render the trace to a file using unicode and ASCII escape sequences. - :param list[str] trace_list: A list of signal names to be output in the specified order. + :param trace_list: A list of signal names to be output in the specified + order. :param file: The place to write output, default to stdout. - :param WaveRenderer renderer: An object that translates traces into output bytes. - :param int symbol_len: The "length" of each rendered value in characters. - If None, the length will be automatically set such that the largest - represented value fits. - :param repr_func: Function to use for representing each value in the trace; - examples are ``hex``, ``oct``, ``bin``, and ``str`` (for decimal), or - the function returned by :py:func:`enum_name`. Defaults to 'hex'. - :param repr_per_name: Map from signal name to a function that takes in the signal's - value and returns a user-defined representation. If a signal name is - not found in the map, the argument `repr_func` will be used instead. - :param int segment_size: Traces are broken in the segments of this number of cycles. + :param renderer: An object that translates traces into output bytes. + :param symbol_len: The "length" of each rendered value in characters. + If ``None``, the length will be automatically set such that the + largest represented value fits. + :param repr_func: Function to use for representing each value in the + trace. Examples include ``hex``, ``oct``, ``bin``, and ``str`` (for + decimal), :py:func:`.val_to_signed_integer` (for signed decimal) or + the function returned by :py:func:`enum_name` (for ``IntEnum``). + Defaults to ``hex``. + :param repr_per_name: Map from signal name to a function that takes in + the signal's value and returns a user-defined representation. If a + signal name is not found in the map, the argument ``repr_func`` + will be used instead. + :param segment_size: Traces are broken in the segments of this number + of cycles. The resulting output can be viewed directly on the terminal or looked - at with :program:`more` or :program:`less -R` which both should handle the ASCII escape - sequences used in rendering. + at with :program:`more` or :program:`less -R` which both should handle + the ASCII escape sequences used in rendering. + """ if _currently_in_jupyter_notebook(): from IPython.display import display, HTML, Javascript # pylint: disable=import-error @@ -1693,12 +1713,15 @@ def formatted_trace_line(wire, trace): "if a CompiledSimulation was used.") if symbol_len is None: - maxvallen = 0 + max_symbol_len = 0 for trace_name in trace_list: trace = self.trace[trace_name] - maxvallen = max(maxvallen, max(len(renderer.val_to_str( - v, trace_name, repr_func, repr_per_name)) for v in trace)) - symbol_len = maxvallen + current_symbol_len = max( + len(renderer.val_to_str( + v, self._wires[trace_name], repr_func, repr_per_name)) + for v in trace) + max_symbol_len = max(max_symbol_len, current_symbol_len) + symbol_len = max_symbol_len cycle_len = symbol_len + renderer.constants._chars_between_cycles diff --git a/tests/test_simulation.py b/tests/test_simulation.py index dc2fabfa..1c429b52 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -254,6 +254,22 @@ class State(enum.IntEnum): ) self.assertEqual(buff.getvalue(), expected) + def test_val_to_signed_integer(self): + bitwidth = 2 + counter = pyrtl.Register(name='counter', bitwidth=bitwidth) + counter.next <<= counter + 1 + sim = pyrtl.Simulation() + sim.step_multiple(nsteps=2 ** bitwidth) + buff = io.StringIO() + sim.tracer.render_trace(file=buff, renderer=self.renderer, + repr_func=pyrtl.val_to_signed_integer) + expected = ( + " |0 |1 |2 |3 \n" + " \n" + "counter --|1 |-2|-1\n" + ) + self.assertEqual(buff.getvalue(), expected) + def test_custom_repr_per_wire(self): class Foo(enum.IntEnum): A = 0