Skip to content

Commit

Permalink
Rewrite conditional_assignment documentation:
Browse files Browse the repository at this point in the history
- Expand explanation of `conditional_assignment` defaults.
- Explain how conditional_assignment works with `MemBlock`s.
- Explain how the conditional assignment operator (`|=`) differs from the
  unconditional assignment operator (`<<=`).
- Add more examples.

Also:

- Update documentation `Makefile` to install or update `pip-compile`.
- Update documentation dependencies.
  • Loading branch information
fdxmw committed Dec 19, 2024
1 parent 4151a55 commit 51fe85b
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 79 deletions.
1 change: 1 addition & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ html: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

requirements.txt: requirements.in FORCE
pip install --upgrade pip-tools
pip-compile --upgrade requirements.in

FORCE:
4 changes: 0 additions & 4 deletions docs/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,3 @@ Conditionals
:special-members:
:undoc-members:
:exclude-members: __dict__,__weakref__,__module__

.. autodata:: pyrtl.otherwise

.. autodata:: pyrtl.conditional_assignment
22 changes: 11 additions & 11 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile requirements.in
#
alabaster==1.0.0
# via sphinx
babel==2.15.0
babel==2.16.0
# via sphinx
beautifulsoup4==4.12.3
# via furo
certifi==2024.7.4
certifi==2024.12.14
# via requests
charset-normalizer==3.3.2
charset-normalizer==3.4.0
# via requests
docutils==0.21.2
# via sphinx
furo==2024.8.6
# via -r requirements.in
idna==3.7
idna==3.10
# via requests
imagesize==1.4.1
# via sphinx
jinja2==3.1.4
# via sphinx
markupsafe==2.1.5
markupsafe==3.0.2
# via jinja2
packaging==24.1
packaging==24.2
# via sphinx
pygments==2.18.0
# via
Expand All @@ -36,16 +36,16 @@ requests==2.32.3
# via sphinx
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
soupsieve==2.6
# via beautifulsoup4
sphinx==8.0.2
sphinx==8.1.3
# via
# -r requirements.in
# furo
# sphinx-autodoc-typehints
# sphinx-basic-ng
# sphinx-copybutton
sphinx-autodoc-typehints==2.2.3
sphinx-autodoc-typehints==2.5.0
# via -r requirements.in
sphinx-basic-ng==1.0.0b2
# via furo
Expand All @@ -63,5 +63,5 @@ sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
urllib3==2.2.2
urllib3==2.2.3
# via requests
232 changes: 168 additions & 64 deletions pyrtl/conditional.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,165 @@
"""Conditional assignment of registers and WireVectors based on a predicate.
The management of selected assignments is expected to happen through
the "with" blocks which will ensure that the region of execution for
which the condition should apply is well defined. It is easiest
to see with an example::
r1 = Register()
r2 = Register()
w3 = WireVector()
with conditional_assignment:
with a:
r1.next |= i # set when a is true
with b:
r2.next |= j # set when a and b are true
with c:
r1.next |= k # set when a is false and c is true
r2.next |= k
with otherwise:
r2.next |= l # a is false and c is false
with d:
w3.next |= m # d is true (assignments must be independent)
This is equivalent to::
r1.next <<= select(a, i, select(c, k, default))
r2.next <<= select(a, select(b, j, default), select(c, k, l))
w3 <<= select(d, m, 0)
This functionality is provided through two instances: ``conditional_update``,
which is a context manager (under which conditional assignements can be made),
and ``otherwise``, which is an instance that stands in for a 'fall through'
case. The details of how these should be used, and the difference between
normal assignments and condtional assignments, described in more detail in the
state machine example in ``examples/example3-statemachine.py``.
There are instances where you might want a wirevector to be set to a certain
value in all but certain with blocks. For example, say you have a processor
with a PC register that is normally updated to PC + 1 after each cycle, except
when the current instruction is a branch or jump. You could represent that as
follows::
pc = pyrtl.Register(32)
instr = pyrtl.WireVector(32)
res = pyrtl.WireVector(32)
"""Register and WireVectors can be conditionally assigned values based on predicates.
Conditional assignments are written with `Python with statements
<https://docs.python.org/3/reference/compound_stmts.html#with>`_, using two context
managers:
#. :data:`conditional_assignment`, which provides the framework for specifying
conditional assignments.
#. :data:`otherwise`, which specifies the 'fall through' case.
Conditional assignments are easiest to understand with an example::
r1 = pyrtl.Register(bitwidth=8)
r2 = pyrtl.Register(bitwidth=8)
w = pyrtl.WireVector(bitwidth=8)
mem = pyrtl.MemBlock(bitwidth=8, addrwidth=4)
a = pyrtl.Input(bitwidth=1)
b = pyrtl.Input(bitwidth=1)
c = pyrtl.Input(bitwidth=1)
d = pyrtl.Input(bitwidth=1)
with pyrtl.conditional_assignment:
with a:
# Set when a is True.
r1.next |= 1
mem[0] |= 2
with b:
# Set when a and b are both True.
r2.next |= 3
with c:
# Set when a is False and c is True.
r1.next |= 4
r2.next |= 5
with pyrtl.otherwise:
# Set when a and c are both False.
r2.next |= 6
with d:
# Set when d is True. A `with` block after an `otherwise` starts a new
# set of conditional assignments.
w |= 7
This :data:`conditional_assignment` is equivalent to::
r1.next <<= pyrtl.select(a, 1, pyrtl.select(c, 4, r1))
r2.next <<= pyrtl.select(a, pyrtl.select(b, 3, r2), pyrtl.select(c, 5, 6))
w <<= pyrtl.select(d, 7, 0)
mem[0] <<= pyrtl.MemBlock.EnabledWrite(data=2, enable=a)
Conditional assignments are generally recommended over nested :func:`.select` statements
because conditional assignments are easier to read and write.
-------------------------------
Conditional Assignment Defaults
-------------------------------
Every PyRTL wire, register, and memory must have a value in every cycle. PyRTL does not
support "don't care" or ``X`` values. To satisfy this requirement, conditional
assignment must assign some value to wires in :data:`conditional_assignment` blocks when
a value is not specified. This can happen when:
#. A condition is ``True``, but no value is specified for a wire or register in that
condition's ``with`` block. In the example above, no value is specified for ``r1`` in
the :data:`otherwise` block.
#. No conditions are ``True``, and there is no :data:`otherwise` block. In the example
above, there is no :data:`otherwise` block to for the case when ``d`` is ``False``,
so no value is specified for ``w`` when ``d`` is ``False``.
When this happens for a wire, ``0`` is assigned as a default value. See how a ``0``
appears in the final ``select`` in the equivalent example above.
When this happens for a register, the register's current value is assigned as a default
value. See how ``r1`` and ``r2`` appear within the ``select`` s in the first and second
lines of the example above.
When this happens for a memory, the memory's write port is disabled. See how the example
above uses a :class:`.EnabledWrite` to disable writes to ``mem[0]`` when ``a`` is
``False``.
These default values can be changed by passing a ``defaults`` dict to
:data:`conditional_assignment`, as seen in this example::
# Most instructions advance the program counter (`pc`) by one instruction. A few
# instructions change `pc` in special ways.
pc = pyrtl.Register(bitwidth=32)
instr = pyrtl.WireVector(bitwidth=32)
res = pyrtl.WireVector(bitwidth=32)
op = instr[:7]
ADD = 0b0110011
JMP = 0b1101111
with conditional_assignment(
defaults={
pc: pc + 1,
res: 0
}
):
# Use conditional_assignment's `defaults` to advance `pc` by one instruction by
# default.
with pyrtl.conditional_assignment(defaults={pc: pc + 1}):
with op == ADD:
res |= instr[15:20] + instr[20:25]
# pc will be updated to pc + 1
# pc.next will be updated to pc + 1
with op == JMP:
pc.next |= pc + instr[7:]
# res will be set to 0
In addition to the conditional context, there is a helper function
:func:`~.currently_under_condition` which will test if the code where it is
called is currently elaborating hardware under a condition.
.. WARNING::
:data:`conditional_assignment` ``defaults`` are not supported for
:class:`.MemBlock`.
-------------------------------------------
The Conditional Assigment Operator (``|=``)
-------------------------------------------
Conditional assignments are written with the ``|=`` operator, and not the usual ``<<=``
operator.
* The ``|=`` operator is a *conditional* assignment. Conditional assignments can only be
written in a :data:`conditional_assignment` block.
* The ``<<=`` operator is an *unconditional* assignment, *even if* it is written in a
:data:`conditional_assignment` block.
Consider this example::
w1 = pyrtl.WireVector()
w2 = pyrtl.WireVector()
with pyrtl.conditional_assignment:
with a:
w1 |= 1
w2 <<= 2
Which is equivalent to::
w1 <<= pyrtl.select(a, 1, 0)
w2 <<= 2
This behavior may seem undesirable, but consider this example::
def make_adder(x: pyrtl.WireVector) -> pyrtl.WireVector:
output = pyrtl.WireVector(bitwidth=a.bitwidth + 1)
output <<= x + 2
return output
w = pyrtl.WireVector()
with pyrtl.conditional_assignment:
with a:
w |= make_adder(b)
Which is equivalent to::
# The assignment to `output` in `make_adder` is unconditional.
w <<= pyrtl.select(a, make_adder(b), 0)
In this example the ``<<=`` in ``make_adder`` should be unconditional, even though
``make_adder`` is called from a :data:`conditional_assignment`, because the top-level
assignment to ``w`` is already conditional. Making the lower-level assignment to
``output`` conditional would not make sense, especially if ``output`` is used elsewhere
in the circuit.
For more :data:`conditional_assignment` examples, see the state machine example in
``examples/example3-statemachine.py``.
"""
# Access should be done through instances "conditional_update" and "otherwise",
# as described above, not through the classes themselves.
# Use the objects "conditional_assignment" and "otherwise" as described above. The
# classes below are internal implementation details.

from .pyrtlexceptions import PyrtlError, PyrtlInternalError
from .wire import WireVector, Const, Register
Expand All @@ -82,14 +173,16 @@


def currently_under_condition():
""" Returns True if execution is currently in the context of a ``_ConditionalAssignment.`` """
return _depth > 0
"""Returns ``True`` if execution is currently in the context of a
:data:`conditional_assignment`.
"""
return _depth > 0

# -----------------------------------------------------------------------
# conditional_assignment and otherwise, both visible in the pyrtl module, are defineded as
# instances (hopefully the only and unchanging instances) of the following two types.

# `conditional_assignment` and `otherwise`, both visible in the pyrtl module, are
# defined as instances (hopefully the only and unchanging instances) of the following
# two types.
class _ConditionalAssignment(object):
def __init__(self):
self.defaults = {}
Expand All @@ -114,7 +207,6 @@ def __exit__(self, *exc_info):


class _Otherwise(object):
""" Context providing functionality of PyRTL ``otherwise``. """
def __enter__(self):
_push_condition(otherwise)

Expand Down Expand Up @@ -142,7 +234,19 @@ def _reset_conditional_state():

_reset_conditional_state()
conditional_assignment = _ConditionalAssignment()
"""Context manager implementing PyRTL's ``conditional_assignment``.
:param dict defaults: Dictionary mapping from WireVector to its default value in this
``conditional_assignment`` block. ``defaults`` are not supported for
:class:`.MemBlock`.
"""

otherwise = _Otherwise()
"""Context manager implementing PyRTL's ``otherwise`` under
:data:`conditional_assignment`.
"""


# -----------------------------------------------------------------------
Expand Down

0 comments on commit 51fe85b

Please sign in to comment.