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