Skip to content

Commit 51fe85b

Browse files
committed
Rewrite conditional_assignment documentation:
- 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.
1 parent 4151a55 commit 51fe85b

File tree

4 files changed

+180
-79
lines changed

4 files changed

+180
-79
lines changed

docs/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ html: Makefile
1313
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
1414

1515
requirements.txt: requirements.in FORCE
16+
pip install --upgrade pip-tools
1617
pip-compile --upgrade requirements.in
1718

1819
FORCE:

docs/basic.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,3 @@ Conditionals
6767
:special-members:
6868
:undoc-members:
6969
:exclude-members: __dict__,__weakref__,__module__
70-
71-
.. autodata:: pyrtl.otherwise
72-
73-
.. autodata:: pyrtl.conditional_assignment

docs/requirements.txt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
#
2-
# This file is autogenerated by pip-compile with Python 3.12
2+
# This file is autogenerated by pip-compile with Python 3.13
33
# by the following command:
44
#
55
# pip-compile requirements.in
66
#
77
alabaster==1.0.0
88
# via sphinx
9-
babel==2.15.0
9+
babel==2.16.0
1010
# via sphinx
1111
beautifulsoup4==4.12.3
1212
# via furo
13-
certifi==2024.7.4
13+
certifi==2024.12.14
1414
# via requests
15-
charset-normalizer==3.3.2
15+
charset-normalizer==3.4.0
1616
# via requests
1717
docutils==0.21.2
1818
# via sphinx
1919
furo==2024.8.6
2020
# via -r requirements.in
21-
idna==3.7
21+
idna==3.10
2222
# via requests
2323
imagesize==1.4.1
2424
# via sphinx
2525
jinja2==3.1.4
2626
# via sphinx
27-
markupsafe==2.1.5
27+
markupsafe==3.0.2
2828
# via jinja2
29-
packaging==24.1
29+
packaging==24.2
3030
# via sphinx
3131
pygments==2.18.0
3232
# via
@@ -36,16 +36,16 @@ requests==2.32.3
3636
# via sphinx
3737
snowballstemmer==2.2.0
3838
# via sphinx
39-
soupsieve==2.5
39+
soupsieve==2.6
4040
# via beautifulsoup4
41-
sphinx==8.0.2
41+
sphinx==8.1.3
4242
# via
4343
# -r requirements.in
4444
# furo
4545
# sphinx-autodoc-typehints
4646
# sphinx-basic-ng
4747
# sphinx-copybutton
48-
sphinx-autodoc-typehints==2.2.3
48+
sphinx-autodoc-typehints==2.5.0
4949
# via -r requirements.in
5050
sphinx-basic-ng==1.0.0b2
5151
# via furo
@@ -63,5 +63,5 @@ sphinxcontrib-qthelp==2.0.0
6363
# via sphinx
6464
sphinxcontrib-serializinghtml==2.0.0
6565
# via sphinx
66-
urllib3==2.2.2
66+
urllib3==2.2.3
6767
# via requests

pyrtl/conditional.py

Lines changed: 168 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,165 @@
1-
"""Conditional assignment of registers and WireVectors based on a predicate.
2-
3-
The management of selected assignments is expected to happen through
4-
the "with" blocks which will ensure that the region of execution for
5-
which the condition should apply is well defined. It is easiest
6-
to see with an example::
7-
8-
r1 = Register()
9-
r2 = Register()
10-
w3 = WireVector()
11-
with conditional_assignment:
12-
with a:
13-
r1.next |= i # set when a is true
14-
with b:
15-
r2.next |= j # set when a and b are true
16-
with c:
17-
r1.next |= k # set when a is false and c is true
18-
r2.next |= k
19-
with otherwise:
20-
r2.next |= l # a is false and c is false
21-
22-
with d:
23-
w3.next |= m # d is true (assignments must be independent)
24-
25-
This is equivalent to::
26-
27-
r1.next <<= select(a, i, select(c, k, default))
28-
r2.next <<= select(a, select(b, j, default), select(c, k, l))
29-
w3 <<= select(d, m, 0)
30-
31-
This functionality is provided through two instances: ``conditional_update``,
32-
which is a context manager (under which conditional assignements can be made),
33-
and ``otherwise``, which is an instance that stands in for a 'fall through'
34-
case. The details of how these should be used, and the difference between
35-
normal assignments and condtional assignments, described in more detail in the
36-
state machine example in ``examples/example3-statemachine.py``.
37-
38-
There are instances where you might want a wirevector to be set to a certain
39-
value in all but certain with blocks. For example, say you have a processor
40-
with a PC register that is normally updated to PC + 1 after each cycle, except
41-
when the current instruction is a branch or jump. You could represent that as
42-
follows::
43-
44-
pc = pyrtl.Register(32)
45-
instr = pyrtl.WireVector(32)
46-
res = pyrtl.WireVector(32)
1+
"""Register and WireVectors can be conditionally assigned values based on predicates.
2+
3+
Conditional assignments are written with `Python with statements
4+
<https://docs.python.org/3/reference/compound_stmts.html#with>`_, using two context
5+
managers:
6+
7+
#. :data:`conditional_assignment`, which provides the framework for specifying
8+
conditional assignments.
9+
#. :data:`otherwise`, which specifies the 'fall through' case.
10+
11+
Conditional assignments are easiest to understand with an example::
12+
13+
r1 = pyrtl.Register(bitwidth=8)
14+
r2 = pyrtl.Register(bitwidth=8)
15+
w = pyrtl.WireVector(bitwidth=8)
16+
mem = pyrtl.MemBlock(bitwidth=8, addrwidth=4)
17+
18+
a = pyrtl.Input(bitwidth=1)
19+
b = pyrtl.Input(bitwidth=1)
20+
c = pyrtl.Input(bitwidth=1)
21+
d = pyrtl.Input(bitwidth=1)
22+
23+
with pyrtl.conditional_assignment:
24+
with a:
25+
# Set when a is True.
26+
r1.next |= 1
27+
mem[0] |= 2
28+
with b:
29+
# Set when a and b are both True.
30+
r2.next |= 3
31+
with c:
32+
# Set when a is False and c is True.
33+
r1.next |= 4
34+
r2.next |= 5
35+
with pyrtl.otherwise:
36+
# Set when a and c are both False.
37+
r2.next |= 6
38+
39+
with d:
40+
# Set when d is True. A `with` block after an `otherwise` starts a new
41+
# set of conditional assignments.
42+
w |= 7
43+
44+
This :data:`conditional_assignment` is equivalent to::
45+
46+
r1.next <<= pyrtl.select(a, 1, pyrtl.select(c, 4, r1))
47+
r2.next <<= pyrtl.select(a, pyrtl.select(b, 3, r2), pyrtl.select(c, 5, 6))
48+
w <<= pyrtl.select(d, 7, 0)
49+
mem[0] <<= pyrtl.MemBlock.EnabledWrite(data=2, enable=a)
50+
51+
Conditional assignments are generally recommended over nested :func:`.select` statements
52+
because conditional assignments are easier to read and write.
53+
54+
-------------------------------
55+
Conditional Assignment Defaults
56+
-------------------------------
57+
58+
Every PyRTL wire, register, and memory must have a value in every cycle. PyRTL does not
59+
support "don't care" or ``X`` values. To satisfy this requirement, conditional
60+
assignment must assign some value to wires in :data:`conditional_assignment` blocks when
61+
a value is not specified. This can happen when:
62+
63+
#. A condition is ``True``, but no value is specified for a wire or register in that
64+
condition's ``with`` block. In the example above, no value is specified for ``r1`` in
65+
the :data:`otherwise` block.
66+
#. No conditions are ``True``, and there is no :data:`otherwise` block. In the example
67+
above, there is no :data:`otherwise` block to for the case when ``d`` is ``False``,
68+
so no value is specified for ``w`` when ``d`` is ``False``.
69+
70+
When this happens for a wire, ``0`` is assigned as a default value. See how a ``0``
71+
appears in the final ``select`` in the equivalent example above.
72+
73+
When this happens for a register, the register's current value is assigned as a default
74+
value. See how ``r1`` and ``r2`` appear within the ``select`` s in the first and second
75+
lines of the example above.
76+
77+
When this happens for a memory, the memory's write port is disabled. See how the example
78+
above uses a :class:`.EnabledWrite` to disable writes to ``mem[0]`` when ``a`` is
79+
``False``.
80+
81+
These default values can be changed by passing a ``defaults`` dict to
82+
:data:`conditional_assignment`, as seen in this example::
83+
84+
# Most instructions advance the program counter (`pc`) by one instruction. A few
85+
# instructions change `pc` in special ways.
86+
pc = pyrtl.Register(bitwidth=32)
87+
instr = pyrtl.WireVector(bitwidth=32)
88+
res = pyrtl.WireVector(bitwidth=32)
4789
4890
op = instr[:7]
4991
ADD = 0b0110011
5092
JMP = 0b1101111
5193
52-
with conditional_assignment(
53-
defaults={
54-
pc: pc + 1,
55-
res: 0
56-
}
57-
):
94+
# Use conditional_assignment's `defaults` to advance `pc` by one instruction by
95+
# default.
96+
with pyrtl.conditional_assignment(defaults={pc: pc + 1}):
5897
with op == ADD:
5998
res |= instr[15:20] + instr[20:25]
60-
# pc will be updated to pc + 1
99+
# pc.next will be updated to pc + 1
61100
with op == JMP:
62101
pc.next |= pc + instr[7:]
63102
# res will be set to 0
64103
65-
In addition to the conditional context, there is a helper function
66-
:func:`~.currently_under_condition` which will test if the code where it is
67-
called is currently elaborating hardware under a condition.
104+
.. WARNING::
105+
:data:`conditional_assignment` ``defaults`` are not supported for
106+
:class:`.MemBlock`.
107+
108+
-------------------------------------------
109+
The Conditional Assigment Operator (``|=``)
110+
-------------------------------------------
111+
112+
Conditional assignments are written with the ``|=`` operator, and not the usual ``<<=``
113+
operator.
114+
115+
* The ``|=`` operator is a *conditional* assignment. Conditional assignments can only be
116+
written in a :data:`conditional_assignment` block.
117+
* The ``<<=`` operator is an *unconditional* assignment, *even if* it is written in a
118+
:data:`conditional_assignment` block.
119+
120+
Consider this example::
121+
122+
w1 = pyrtl.WireVector()
123+
w2 = pyrtl.WireVector()
124+
with pyrtl.conditional_assignment:
125+
with a:
126+
w1 |= 1
127+
w2 <<= 2
128+
129+
Which is equivalent to::
130+
131+
w1 <<= pyrtl.select(a, 1, 0)
132+
w2 <<= 2
133+
134+
This behavior may seem undesirable, but consider this example::
135+
136+
def make_adder(x: pyrtl.WireVector) -> pyrtl.WireVector:
137+
output = pyrtl.WireVector(bitwidth=a.bitwidth + 1)
138+
output <<= x + 2
139+
return output
140+
141+
w = pyrtl.WireVector()
142+
with pyrtl.conditional_assignment:
143+
with a:
144+
w |= make_adder(b)
145+
146+
Which is equivalent to::
147+
148+
# The assignment to `output` in `make_adder` is unconditional.
149+
w <<= pyrtl.select(a, make_adder(b), 0)
150+
151+
In this example the ``<<=`` in ``make_adder`` should be unconditional, even though
152+
``make_adder`` is called from a :data:`conditional_assignment`, because the top-level
153+
assignment to ``w`` is already conditional. Making the lower-level assignment to
154+
``output`` conditional would not make sense, especially if ``output`` is used elsewhere
155+
in the circuit.
156+
157+
For more :data:`conditional_assignment` examples, see the state machine example in
158+
``examples/example3-statemachine.py``.
68159
69160
"""
70-
# Access should be done through instances "conditional_update" and "otherwise",
71-
# as described above, not through the classes themselves.
161+
# Use the objects "conditional_assignment" and "otherwise" as described above. The
162+
# classes below are internal implementation details.
72163

73164
from .pyrtlexceptions import PyrtlError, PyrtlInternalError
74165
from .wire import WireVector, Const, Register
@@ -82,14 +173,16 @@
82173

83174

84175
def currently_under_condition():
85-
""" Returns True if execution is currently in the context of a ``_ConditionalAssignment.`` """
86-
return _depth > 0
176+
"""Returns ``True`` if execution is currently in the context of a
177+
:data:`conditional_assignment`.
87178
179+
"""
180+
return _depth > 0
88181

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

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

115208

116209
class _Otherwise(object):
117-
""" Context providing functionality of PyRTL ``otherwise``. """
118210
def __enter__(self):
119211
_push_condition(otherwise)
120212

@@ -142,7 +234,19 @@ def _reset_conditional_state():
142234

143235
_reset_conditional_state()
144236
conditional_assignment = _ConditionalAssignment()
237+
"""Context manager implementing PyRTL's ``conditional_assignment``.
238+
239+
:param dict defaults: Dictionary mapping from WireVector to its default value in this
240+
``conditional_assignment`` block. ``defaults`` are not supported for
241+
:class:`.MemBlock`.
242+
243+
"""
244+
145245
otherwise = _Otherwise()
246+
"""Context manager implementing PyRTL's ``otherwise`` under
247+
:data:`conditional_assignment`.
248+
249+
"""
146250

147251

148252
# -----------------------------------------------------------------------

0 commit comments

Comments
 (0)