Skip to content

Commit

Permalink
Many fixes and improvements to PyRTL's handling of signed integers:
Browse files Browse the repository at this point in the history
- Allow negative numbers as simulation input values in `step()` and
  `step_multiple()`.
- Define `signed_sub`. WireVectors support `+`, `-`, and `*`, but we only had
  `signed_add` and `signed_mult`. Adding `signed_sub` makes these sets
  symmetric.
- Enable test_signed.py. It was missing a call to `unittest.main()`. Add more
  tests for signed arithmetic.
- Simplify definitions of `signed_add` and `signed_mult`.
- Improve `infer_val_and_bitwidth`'s error messages.
- Add example1.1, which demonstrates:
  - Passing signed integers as Inputs.
  - Correctly adding signed integers.
  - Displaying signed integers in traces.
- Many documentation updates.
  • Loading branch information
fdxmw committed May 17, 2024
1 parent 1bf0b9e commit 3bb52d5
Show file tree
Hide file tree
Showing 12 changed files with 673 additions and 376 deletions.
3 changes: 2 additions & 1 deletion docs/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ WireVector
.. autoclass:: pyrtl.wire.WireVector
:members:
:special-members: __init__, __add__, __sub__, __mul__, __getitem___,
__len__, __ilshift__
__len__, __ilshift__, __and__, __or__, __xor__, __lt__,
__le__, __eq__, __ne__, __gt__, __ge__, __len__

Input Pins
----------
Expand Down
7 changes: 4 additions & 3 deletions docs/helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ Extended Logic and Arithmetic

The functions below provide ways of comparing and arithmetically combining
:class:`WireVectors<.WireVector>` in ways that are often useful in hardware
design. The functions below extend those member functions of the
:class:`.WireVector` class itself (which provides support for addition,
unsigned multiplication, unsigned comparison, and many others).
design. The functions below extend those member functions of the
:class:`.WireVector` class itself (which provides support for unsigned
addition, subtraction, multiplication, comparison, and many others).

.. autofunction:: pyrtl.corecircuits.signed_add
.. autofunction:: pyrtl.corecircuits.signed_sub
.. autofunction:: pyrtl.corecircuits.signed_mult
.. autofunction:: pyrtl.corecircuits.signed_lt
.. autofunction:: pyrtl.corecircuits.signed_le
Expand Down
3 changes: 0 additions & 3 deletions examples/example1-combologic.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,3 @@
or python_cout != sim_trace.trace['carry_out'][cycle]):
print('This Example is Broken!!!')
exit(1)

# You made it to the end!
exit(0)
121 changes: 121 additions & 0 deletions examples/example1.1-signed-numbers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Example 1.1: Working with signed integers.
This example demonstrates:
• Correct addition of signed integers with `signed_add`.
• Displaying signed integers in traces with `val_to_signed_integer`.
Signed integers are represented in two's complement.
https://en.wikipedia.org/wiki/Two%27s_complement
"""

import pyrtl

# Let's start with unsigned addition.
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
# Add all combinations of two unsigned 2-bit inputs.
a = pyrtl.Input(bitwidth=2, name='a')
b = pyrtl.Input(bitwidth=2, name='b')

unsigned_sum = pyrtl.Output(bitwidth=3, name='unsigned_sum')
unsigned_sum <<= a + b

# Try all combinations of {0, 1, 2, 3} + {0, 1, 2, 3}.
unsigned_inputs = {
'a': [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3],
'b': [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
}

sim = pyrtl.Simulation()
sim.step_multiple(provided_inputs=unsigned_inputs)
# In this trace, `unsigned_sum` is the sum of all combinations of
# {0, 1, 2, 3} + {0, 1, 2, 3}. For example:
# • cycle 0 shows 0 + 0 = 0
# • cycle 1 shows 0 + 1 = 1
# • cycle 15 shows 3 + 3 = 6
print('Unsigned addition. Each cycle adds a different combination of '
'numbers.\nunsigned_sum == a + b')
sim.tracer.render_trace(repr_func=int)

# Re-interpreting `a`, `b`, and `unsigned_sum` as signed is incorrect.
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
# Use `val_to_signed_integer` to re-interpret the previous simulation results
# as signed integers. But the results are INCORRECT, because `unsigned_sum`
# performed unsigned addition with `+`. For example:
#
# • cycle 2 shows 0 + -2 = 2
# • cycle 13 shows -1 + 1 = -4
#
# `unsigned_sum` is incorrect because PyRTL must extend `a` and `b` to the
# sum's bitwidth before adding, but PyRTL zero-extends by default, instead of
# sign-extending. Zero-extending is correct when `a` and `b` are unsigned, but
# sign-extending is correct when `a` and `b` are signed. Use `signed_add` to
# sign-extend `a` and `b`, as demonstrated next.
print('\nUse `val_to_signed_integer` to re-interpret the previous simulation '
'results as\nsigned integers. But re-interpreting the previous trace as '
'signed integers \nproduces INCORRECT RESULTS!')
sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer)

# Use `signed_add` to correctly add signed integers.
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
# `signed_add` works by sign-extending its inputs to the sum's bitwidth before
# adding. There are many `signed_*` functions for signed operations, like
# `signed_sub`, `signed_mul`, `signed_lt`, and so on.
pyrtl.reset_working_block()

a = pyrtl.Input(bitwidth=2, name='a')
b = pyrtl.Input(bitwidth=2, name='b')

signed_sum = pyrtl.Output(bitwidth=3, name='signed_sum')
signed_sum <<= pyrtl.signed_add(a, b)

# Try all combinations of {-2, -1, 0, 1} + {-2, -1, 0, 1}.
signed_inputs = {
'a': [-2, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1],
'b': [-2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1]
}
sim = pyrtl.Simulation()
sim.step_multiple(provided_inputs=signed_inputs)

# In this trace, `signed_sum` is the sum of all combinations of
# {-2, -1, 0, 1} + {-2, -1, 0, 1}. For example:
# • cycle 0 shows -2 + -2 = -4
# • cycle 1 shows -2 + -1 = -3
# • cycle 15 shows 1 + 1 = 2
print('\nReset the simulation and use `signed_add` to correctly add signed '
'integers.\nsigned_sum == signed_add(a, b)')
sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer)

# Manually sign-extend inputs to correctly add signed integers with `+`.
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
# Instead of using `signed_add`, we can manually sign-extend the inputs and
# truncate the output to correctly add signed integers with `+`.
#
# Use this trick to implement other signed arithmetic operations.
pyrtl.reset_working_block()

a = pyrtl.Input(bitwidth=2, name='a')
b = pyrtl.Input(bitwidth=2, name='b')

# Using `+` produces the correct result for signed addition here because we
# manually extend `a` and `b` to 3 bits. The result of `a.sign_extended(3) +
# b.sign_extended(3)` is now 4 bits, but we truncate it to 3 bits. This
# truncation is important! The addition's full 4 bit result would not be
# correct.
sign_extended_sum = pyrtl.Output(bitwidth=3, name='sign_extended_sum')
extended_a = a.sign_extended(bitwidth=3)
extended_b = b.sign_extended(bitwidth=3)
sign_extended_sum <<= (extended_a + extended_b).truncate(bitwidth=3)

# Try all combinations of {-2, -1, 0, 1} + {-2, -1, 0, 1}.
signed_inputs = {
'a': [-2, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1],
'b': [-2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1]
}
sim = pyrtl.Simulation()
sim.step_multiple(provided_inputs=signed_inputs)

print('\nInstead of using `signed_add`, we can also manually sign extend the '
'inputs to\ncorrectly add signed integers with `+`.\n'
'sign_extended_sum == a.sign_extended(3) + b.sign_extended(3)')
sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer)
1 change: 1 addition & 0 deletions pyrtl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from .corecircuits import bitfield_update
from .corecircuits import bitfield_update_set
from .corecircuits import signed_add
from .corecircuits import signed_sub
from .corecircuits import signed_mult
from .corecircuits import signed_lt
from .corecircuits import signed_le
Expand Down
145 changes: 90 additions & 55 deletions pyrtl/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,61 +39,96 @@ class LogicNet(collections.namedtuple('LogicNet', ['op', 'op_param', 'args', 'de
Logical Operations:
===== ======== ======== ====== ====
op op_param args dests
===== ======== ======== ====== ====
``&`` `None` `a1, a2` `out` AND two wires together, put result into `out`
``|`` `None` `a1, a2` `out` OR two wires together, put result into `out`
``^`` `None` `a1, a2` `out` XOR two wires together, put result into `out`
``n`` `None` `a1, a2` `out` NAND two wires together, put result into `out`
``~`` `None` `a1` `out` invert one wire, put result into `out`
``+`` `None` `a1, a2` `out` add `a1` and `a2`, put result into `out`
``len(out) == max(len(a1), len(a2)) + 1``
works with both unsigned and two's complement
``-`` `None` `a1, a2` `out` subtract `a2` from `a1`, put result into `out`
``len(out) == max(len(a1), len(a2)) + 1``
works with both unsigned and two's complement
``*`` `None` `a1, a2` `out` multiply `a1` & `a2`, put result into `out`
``len(out) == len(a1) + len(a2)``
assumes unsigned, but :func:`.signed_mult` provides wrapper
``=`` `None` `a1, a2` `out` check `a1` & `a2` equal, put result into `out` (0 | 1)
``<`` `None` `a1, a2` `out` check `a2` greater than `a1`, put result into `out` (0 | 1)
``>`` `None` `a1, a2` `out` check `a1` greater than `a2`, put result into `out` (0 | 1)
``w`` `None` `w1` `w2` connects `w1` to `w2`
directional wire with no logical operation
``x`` `None` `x`, `out` multiplexer:
when `x` == 0 connect `a1` to `out`
when `x` == 1 connect `a2` to `out`
`x` must be one bit and ``len(a1) == len(a2)``
`a1, a2`
``c`` `None` `\\*args` `out` concatenates `\\*args` (wires) into single WireVector
puts first arg at MSB, last arg at LSB
``s`` `sel` `wire` `out` selects bits from wire based on `sel` (slicing syntax)
puts selected bits into `out`
``r`` `None` `next` `r1` on positive clock edge: copies `next` to `r1`
``m`` `memid`, `addr` `data` read address addr of mem (w/ id `memid`), put it into `data`
`mem`
``@`` `memid`, `addr` write data to mem (w/ id `memid`) at address `addr`
request write enable (`wr_en`)
`mem` `data`,
`wr_en`
===== ======== ======== ====== ====
===== ========== ========== ======== ====
op op_param args dests
===== ========== ========== ======== ====
``&`` ``None`` ``a1, a2`` ``out`` AND two wires together, put result
into ``out``
``|`` ``None`` ``a1, a2`` ``out`` OR two wires together, put result into
``out``
``^`` ``None`` ``a1, a2`` ``out`` XOR two wires together, put result
into ``out``
``n`` ``None`` ``a1, a2`` ``out`` NAND two wires together, put result
into ``out``
``~`` ``None`` ``a1`` ``out`` invert one wire, put result into
``out``
``+`` ``None`` ``a1, a2`` ``out`` add ``a1`` and ``a2``, put result into
``out``
``len(out) == max(len(a1), len(a2)) + 1``
Performs *unsigned* addition. Use
:func:`.signed_add` for signed
addition.
``-`` ``None`` ``a1, a2`` ``out`` subtract ``a2`` from ``a1``, put
result into ``out``
``len(out) == max(len(a1), len(a2)) + 1``
Performs *unsigned* subtraction. Use
:func:`.signed_sub` for signed
subtraction.
``*`` ``None`` ``a1, a2`` ``out`` multiply ``a1`` & ``a2``, put result
into ``out``
``len(out) == len(a1) + len(a2)``
Performs *unsigned* multiplication.
Use :func:`.signed_mult` for signed
multiplication.
``=`` ``None`` ``a1, a2`` ``out`` check ``a1`` & ``a2`` equal, put
result into ``out`` (0 | 1)
``<`` ``None`` ``a1, a2`` ``out`` check ``a1`` less than ``a2``, put
result into ``out``. ``out`` has
bitwidth 1.
Performs *unsigned* comparison. Use
:func:`.signed_lt` for signed less
than.
``>`` ``None`` ``a1, a2`` ``out`` check ``a1`` greater than ``a2``, put
result into ``out`` ``out`` has
bitwidth 1.
Performs *unsigned* comparison. Use
:func:`.signed_gt` for signed greater
than.
``w`` ``None`` ``w1`` ``w2`` connects ``w1`` to ``w2``
directional wire with no logical
operation
``x`` ``None`` ``x``, ``out`` multiplexer:
when ``x`` == 0 connect ``a1`` to
``out``
when ``x`` == 1 connect ``a2`` to
``out``
``x`` must be one bit and ``len(a1) == len(a2)``
``a1, a2``
``c`` ``None`` ``*args`` ``out`` concatenates ``*args`` (wires) into
single WireVector
puts first arg at MSB, last arg at LSB
``s`` ``sel`` ``wire`` ``out`` selects bits from wire based on
``sel`` (slicing syntax)
puts selected bits into ``out``
``r`` ``None`` ``next`` ``r1`` on positive clock edge: copies
``next`` to ``r1``
``m`` ``memid``, ``addr`` ``data`` read address addr of mem (with id
``memid``), put it into ``data``
``mem``
``@`` ``memid``, ``addr`` write data to mem (with id ``memid``)
at address ``addr``
request write enable (``wr_en``)
``mem`` ``data``,
``wr_en``
===== ========== ========== ======== ====
"""

Expand Down
Loading

0 comments on commit 3bb52d5

Please sign in to comment.