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