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