Remove comment sign and add correct path for nmigen intersphinx.
[nmutil.git] / src / nmutil / stageapi.py
1 """ Stage API
2
3 This work is funded through NLnet under Grant 2019-02-012
4
5 License: LGPLv3+
6
7
8 Associated development bugs:
9 * http://bugs.libre-riscv.org/show_bug.cgi?id=148
10 * http://bugs.libre-riscv.org/show_bug.cgi?id=64
11 * http://bugs.libre-riscv.org/show_bug.cgi?id=57
12
13 Stage API:
14 ---------
15
16 stage requires compliance with a strict API that may be
17 implemented in several means, including as a static class.
18
19 Stages do not HOLD data, and they definitely do not contain
20 signalling (ready/valid). They do however specify the FORMAT
21 of the incoming and outgoing data, and they provide a means to
22 PROCESS that data (from incoming format to outgoing format).
23
24 Stage Blocks really should be combinatorial blocks (Moore FSMs).
25 It would be ok to have input come in from sync'd sources
26 (clock-driven, Mealy FSMs) however by doing so they would no longer
27 be deterministic, and chaining such blocks with such side-effects
28 together could result in unexpected, unpredictable, unreproduceable
29 behaviour.
30
31 So generally to be avoided, then unless you know what you are doing.
32 https://en.wikipedia.org/wiki/Moore_machine
33 https://en.wikipedia.org/wiki/Mealy_machine
34
35 the methods of a stage instance must be as follows:
36
37 * ispec() - Input data format specification. Takes a bit of explaining.
38 The requirements are: something that eventually derives from
39 nmigen Value must be returned *OR* an iterator or iterable
40 or sequence (list, tuple etc.) or generator must *yield*
41 thing(s) that (eventually) derive from the nmigen Value class.
42
43 Complex to state, very simple in practice:
44 see test_buf_pipe.py for over 25 worked examples.
45
46 * ospec() - Output data format specification.
47 format requirements identical to ispec.
48
49 * process(m, i) - Optional function for processing ispec-formatted data.
50 returns a combinatorial block of a result that
51 may be assigned to the output, by way of the "nmoperator.eq"
52 function. Note that what is returned here can be
53 extremely flexible. Even a dictionary can be returned
54 as long as it has fields that match precisely with the
55 Record into which its values is intended to be assigned.
56 Again: see example unit tests for details.
57
58 * setup(m, i) - Optional function for setting up submodules.
59 may be used for more complex stages, to link
60 the input (i) to submodules. must take responsibility
61 for adding those submodules to the module (m).
62 the submodules must be combinatorial blocks and
63 must have their inputs and output linked combinatorially.
64
65 Both StageCls (for use with non-static classes) and Stage (for use
66 by static classes) are abstract classes from which, for convenience
67 and as a courtesy to other developers, anything conforming to the
68 Stage API may *choose* to derive. See Liskov Substitution Principle:
69 https://en.wikipedia.org/wiki/Liskov_substitution_principle
70
71 StageChain:
72 ----------
73
74 A useful combinatorial wrapper around stages that chains them together
75 and then presents a Stage-API-conformant interface. By presenting
76 the same API as the stages it wraps, it can clearly be used recursively.
77
78 StageHelper:
79 ----------
80
81 A convenience wrapper around a Stage-API-compliant "thing" which
82 complies with the Stage API and provides mandatory versions of
83 all the optional bits.
84 """
85
86 from nmigen import Elaboratable
87 from abc import ABCMeta, abstractmethod
88 import inspect
89
90 from nmutil import nmoperator
91
92
93 def _spec(fn, name=None):
94 """ useful function that determines if "fn" has an argument "name".
95 if so, fn(name) is called otherwise fn() is called.
96
97 means that ispec and ospec can be declared with *or without*
98 a name argument. normally it would be necessary to have
99 "ispec(name=None)" to achieve the same effect.
100 """
101 if name is None:
102 return fn()
103 varnames = dict(inspect.getmembers(fn.__code__))['co_varnames']
104 if 'name' in varnames:
105 return fn(name=name)
106 return fn()
107
108
109 class StageCls(metaclass=ABCMeta):
110 """ Class-based "Stage" API. requires instantiation (after derivation)
111
112 see "Stage API" above.. Note: python does *not* require derivation
113 from this class. All that is required is that the pipelines *have*
114 the functions listed in this class. Derivation from this class
115 is therefore merely a "courtesy" to maintainers.
116 """
117 @abstractmethod
118 def ispec(self): pass # REQUIRED
119 @abstractmethod
120 def ospec(self): pass # REQUIRED
121 #@abstractmethod
122 #def setup(self, m, i): pass # OPTIONAL
123 #@abstractmethod
124 #def process(self, i): pass # OPTIONAL
125
126
127 class Stage(metaclass=ABCMeta):
128 """ Static "Stage" API. does not require instantiation (after derivation)
129
130 see "Stage API" above. Note: python does *not* require derivation
131 from this class. All that is required is that the pipelines *have*
132 the functions listed in this class. Derivation from this class
133 is therefore merely a "courtesy" to maintainers.
134 """
135 @staticmethod
136 @abstractmethod
137 def ispec(): pass
138
139 @staticmethod
140 @abstractmethod
141 def ospec(): pass
142
143 #@staticmethod
144 #@abstractmethod
145 #def setup(m, i): pass
146
147 #@staticmethod
148 #@abstractmethod
149 #def process(i): pass
150
151
152 class StageHelper(Stage):
153 """ a convenience wrapper around something that is Stage-API-compliant.
154 (that "something" may be a static class, for example).
155
156 StageHelper happens to also be compliant with the Stage API,
157 it differs from the stage that it wraps in that all the "optional"
158 functions are provided (hence the designation "convenience wrapper")
159 """
160 def __init__(self, stage):
161 self.stage = stage
162 self._ispecfn = None
163 self._ospecfn = None
164 if stage is not None:
165 self.set_specs(self, self)
166
167 def ospec(self, name=None):
168 assert self._ospecfn is not None
169 return _spec(self._ospecfn, name)
170
171 def ispec(self, name=None):
172 assert self._ispecfn is not None
173 return _spec(self._ispecfn, name)
174
175 def set_specs(self, p, n):
176 """ sets up the ispecfn and ospecfn for getting input and output data
177 """
178 if hasattr(p, "stage"):
179 p = p.stage
180 if hasattr(n, "stage"):
181 n = n.stage
182 self._ispecfn = p.ispec
183 self._ospecfn = n.ospec
184
185 def new_specs(self, name):
186 """ allocates new ispec and ospec pair
187 """
188 return (_spec(self.ispec, "%s_i" % name),
189 _spec(self.ospec, "%s_o" % name))
190
191 def process(self, i):
192 if self.stage and hasattr(self.stage, "process"):
193 return self.stage.process(i)
194 return i
195
196 def setup(self, m, i):
197 if self.stage is not None and hasattr(self.stage, "setup"):
198 self.stage.setup(m, i)
199
200 def _postprocess(self, i): # XXX DISABLED
201 return i # RETURNS INPUT
202 if hasattr(self.stage, "postprocess"):
203 return self.stage.postprocess(i)
204 return i
205
206
207 class StageChain(StageHelper):
208 """ pass in a list of stages (combinatorial blocks), and they will
209 automatically be chained together via their input and output specs
210 into a combinatorial chain, to create one giant combinatorial
211 block.
212
213 the end result conforms to the exact same Stage API.
214
215 * input to this class will be the input of the first stage
216 * output of first stage goes into input of second
217 * output of second goes into input into third
218 * ... (etc. etc.)
219 * the output of this class will be the output of the last stage
220
221 NOTE: whilst this is very similar to ControlBase.connect(), it is
222 *really* important to appreciate that StageChain is pure
223 combinatorial and bypasses (does not involve, at all, ready/valid
224 signalling OF ANY KIND).
225
226 ControlBase.connect on the other hand respects, connects, and uses
227 ready/valid signalling.
228
229 Arguments:
230
231 * :chain: a chain of combinatorial blocks conforming to the Stage API
232 NOTE: StageChain.ispec and ospect have to have something
233 to return (beginning and end specs of the chain),
234 therefore the chain argument must be non-zero length
235
236 * :specallocate: if set, new input and output data will be allocated
237 and connected (eq'd) to each chained Stage.
238 in some cases if this is not done, the nmigen warning
239 "driving from two sources, module is being flattened"
240 will be issued.
241
242 NOTE: DO NOT use StageChain with combinatorial blocks that have
243 side-effects (state-based / clock-based input) or conditional
244 (inter-chain) dependencies, unless you really know what you are doing.
245 """
246 def __init__(self, chain, specallocate=False):
247 assert len(chain) > 0, "stage chain must be non-zero length"
248 self.chain = chain
249 StageHelper.__init__(self, None)
250 if specallocate:
251 self.setup = self._sa_setup
252 else:
253 self.setup = self._na_setup
254 self.set_specs(self.chain[0], self.chain[-1])
255
256 def _sa_setup(self, m, i):
257 for (idx, c) in enumerate(self.chain):
258 if hasattr(c, "setup"):
259 c.setup(m, i) # stage may have some module stuff
260 ofn = self.chain[idx].ospec # last assignment survives
261 cname = 'chainin%d' % idx
262 o = _spec(ofn, cname)
263 if isinstance(o, Elaboratable):
264 setattr(m.submodules, cname, o)
265 m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
266 if idx == len(self.chain)-1:
267 break
268 ifn = self.chain[idx+1].ispec # new input on next loop
269 i = _spec(ifn, 'chainin%d' % (idx+1))
270 m.d.comb += nmoperator.eq(i, o) # assign to next input
271 self.o = o
272 return self.o # last loop is the output
273
274 def _na_setup(self, m, i):
275 for (idx, c) in enumerate(self.chain):
276 if hasattr(c, "setup"):
277 c.setup(m, i) # stage may have some module stuff
278 i = o = c.process(i) # store input into "o"
279 self.o = o
280 return self.o # last loop is the output
281
282 def process(self, i):
283 return self.o # conform to Stage API: return last-loop output
284
285