bc79071046b5d358e4c9c005b9704be3c48f2ff8
[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
161 def __init__(self, stage):
162 self.stage = stage
163 self._ispecfn = None
164 self._ospecfn = None
165 if stage is not None:
166 self.set_specs(self, self)
167
168 def ospec(self, name=None):
169 assert self._ospecfn is not None
170 return _spec(self._ospecfn, name)
171
172 def ispec(self, name=None):
173 assert self._ispecfn is not None
174 return _spec(self._ispecfn, name)
175
176 def set_specs(self, p, n):
177 """ sets up the ispecfn and ospecfn for getting input and output data
178 """
179 if hasattr(p, "stage"):
180 p = p.stage
181 if hasattr(n, "stage"):
182 n = n.stage
183 self._ispecfn = p.ispec
184 self._ospecfn = n.ospec
185
186 def new_specs(self, name):
187 """ allocates new ispec and ospec pair
188 """
189 return (_spec(self.ispec, "%s_i" % name),
190 _spec(self.ospec, "%s_o" % name))
191
192 def process(self, i):
193 if self.stage and hasattr(self.stage, "process"):
194 return self.stage.process(i)
195 return i
196
197 def setup(self, m, i):
198 if self.stage is not None and hasattr(self.stage, "setup"):
199 self.stage.setup(m, i)
200
201 def _postprocess(self, i): # XXX DISABLED
202 return i # RETURNS INPUT
203 if hasattr(self.stage, "postprocess"):
204 return self.stage.postprocess(i)
205 return i
206
207
208 class StageChain(StageHelper):
209 """ pass in a list of stages (combinatorial blocks), and they will
210 automatically be chained together via their input and output specs
211 into a combinatorial chain, to create one giant combinatorial
212 block.
213
214 the end result conforms to the exact same Stage API.
215
216 * input to this class will be the input of the first stage
217 * output of first stage goes into input of second
218 * output of second goes into input into third
219 * ... (etc. etc.)
220 * the output of this class will be the output of the last stage
221
222 NOTE: whilst this is very similar to ControlBase.connect(), it is
223 *really* important to appreciate that StageChain is pure
224 combinatorial and bypasses (does not involve, at all, ready/valid
225 signalling OF ANY KIND).
226
227 ControlBase.connect on the other hand respects, connects, and uses
228 ready/valid signalling.
229
230 Arguments:
231
232 * :chain: a chain of combinatorial blocks conforming to the Stage API
233 NOTE: StageChain.ispec and ospect have to have something
234 to return (beginning and end specs of the chain),
235 therefore the chain argument must be non-zero length
236
237 * :specallocate: if set, new input and output data will be allocated
238 and connected (eq'd) to each chained Stage.
239 in some cases if this is not done, the nmigen warning
240 "driving from two sources, module is being flattened"
241 will be issued.
242
243 NOTE: DO NOT use StageChain with combinatorial blocks that have
244 side-effects (state-based / clock-based input) or conditional
245 (inter-chain) dependencies, unless you really know what you are doing.
246 """
247
248 def __init__(self, chain, specallocate=False):
249 assert len(chain) > 0, "stage chain must be non-zero length"
250 self.chain = chain
251 StageHelper.__init__(self, None)
252 if specallocate:
253 self.setup = self._sa_setup
254 else:
255 self.setup = self._na_setup
256 self.set_specs(self.chain[0], self.chain[-1])
257
258 def _sa_setup(self, m, i):
259 for (idx, c) in enumerate(self.chain):
260 if hasattr(c, "setup"):
261 c.setup(m, i) # stage may have some module stuff
262 ofn = self.chain[idx].ospec # last assignment survives
263 cname = 'chainin%d' % idx
264 o = _spec(ofn, cname)
265 if isinstance(o, Elaboratable):
266 setattr(m.submodules, cname, o)
267 # process input into "o"
268 m.d.comb += nmoperator.eq(o, c.process(i))
269 if idx == len(self.chain)-1:
270 break
271 ifn = self.chain[idx+1].ispec # new input on next loop
272 i = _spec(ifn, 'chainin%d' % (idx+1))
273 m.d.comb += nmoperator.eq(i, o) # assign to next input
274 self.o = o
275 return self.o # last loop is the output
276
277 def _na_setup(self, m, i):
278 for (idx, c) in enumerate(self.chain):
279 if hasattr(c, "setup"):
280 c.setup(m, i) # stage may have some module stuff
281 i = o = c.process(i) # store input into "o"
282 self.o = o
283 return self.o # last loop is the output
284
285 def process(self, i):
286 return self.o # conform to Stage API: return last-loop output