259b9de92a9ce20a81f792321ba959cc6f99bbb3
[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 import nmoperator
80
81
82 def _spec(fn, name=None):
83 if name is None:
84 return fn()
85 varnames = dict(inspect.getmembers(fn.__code__))['co_varnames']
86 if 'name' in varnames:
87 return fn(name=name)
88 return fn()
89
90
91 class StageCls(metaclass=ABCMeta):
92 """ Class-based "Stage" API. requires instantiation (after derivation)
93
94 see "Stage API" above.. Note: python does *not* require derivation
95 from this class. All that is required is that the pipelines *have*
96 the functions listed in this class. Derivation from this class
97 is therefore merely a "courtesy" to maintainers.
98 """
99 @abstractmethod
100 def ispec(self): pass # REQUIRED
101 @abstractmethod
102 def ospec(self): pass # REQUIRED
103 #@abstractmethod
104 #def setup(self, m, i): pass # OPTIONAL
105 #@abstractmethod
106 #def process(self, i): pass # OPTIONAL
107
108
109 class Stage(metaclass=ABCMeta):
110 """ Static "Stage" API. does not require 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 @staticmethod
118 @abstractmethod
119 def ispec(): pass
120
121 @staticmethod
122 @abstractmethod
123 def ospec(): pass
124
125 #@staticmethod
126 #@abstractmethod
127 #def setup(m, i): pass
128
129 #@staticmethod
130 #@abstractmethod
131 #def process(i): pass
132
133
134 class StageHelper(Stage):
135 """ a convenience wrapper around something that is Stage-API-compliant.
136 (that "something" may be a static class, for example).
137
138 StageHelper happens to also be compliant with the Stage API,
139 it differs from the stage that it wraps in that all the "optional"
140 functions are provided (hence the designation "convenience wrapper")
141 """
142 def __init__(self, stage):
143 self.stage = stage
144 self._ispecfn = None
145 self._ospecfn = None
146 if stage is not None:
147 self.set_specs(self, self)
148
149 def ospec(self, name):
150 assert self._ospecfn is not None
151 return _spec(self._ospecfn, name)
152
153 def ispec(self, name):
154 assert self._ispecfn is not None
155 return _spec(self._ispecfn, name)
156
157 def set_specs(self, p, n):
158 self._ispecfn = p.stage.ispec
159 self._ospecfn = n.stage.ospec
160
161 def new_specs(self, name):
162 """ allocates new ispec and ospec pair
163 """
164 return self.ispec("%s_i" % name), self.ospec("%s_o" % name)
165
166 def process(self, i):
167 if self.stage and hasattr(self.stage, "process"):
168 return self.stage.process(i)
169 return i
170
171 def setup(self, m, i):
172 if self.stage is not None and hasattr(self.stage, "setup"):
173 self.stage.setup(m, i)
174
175 def _postprocess(self, i): # XXX DISABLED
176 return i # RETURNS INPUT
177 if hasattr(self.stage, "postprocess"):
178 return self.stage.postprocess(i)
179 return i
180
181
182 class StageChain(StageCls):
183 """ pass in a list of stages, and they will automatically be
184 chained together via their input and output specs into a
185 combinatorial chain, to create one giant combinatorial block.
186
187 the end result basically conforms to the exact same Stage API.
188
189 * input to this class will be the input of the first stage
190 * output of first stage goes into input of second
191 * output of second goes into input into third
192 * ... (etc. etc.)
193 * the output of this class will be the output of the last stage
194
195 NOTE: whilst this is very similar to ControlBase.connect(), it is
196 *really* important to appreciate that StageChain is pure
197 combinatorial and bypasses (does not involve, at all, ready/valid
198 signalling of any kind).
199
200 ControlBase.connect on the other hand respects, connects, and uses
201 ready/valid signalling.
202
203 Arguments:
204
205 * :chain: a chain of combinatorial blocks conforming to the Stage API
206 NOTE: StageChain.ispec and ospect have to have something
207 to return (beginning and end specs of the chain),
208 therefore the chain argument must be non-zero length
209
210 * :specallocate: if set, new input and output data will be allocated
211 and connected (eq'd) to each chained Stage.
212 in some cases if this is not done, the nmigen warning
213 "driving from two sources, module is being flattened"
214 will be issued.
215
216 NOTE: do NOT use StageChain with combinatorial blocks that have
217 side-effects (state-based / clock-based input) or conditional
218 (inter-chain) dependencies, unless you really know what you are doing.
219 """
220 def __init__(self, chain, specallocate=False):
221 assert len(chain) > 0, "stage chain must be non-zero length"
222 self.chain = chain
223 self.setup = self._sa_setup if specallocate else self._na_setup
224
225 def ispec(self):
226 """ returns the ispec of the first of the chain
227 """
228 return _spec(self.chain[0].ispec, "chainin")
229
230 def ospec(self):
231 """ returns the ospec of the last of the chain
232 """
233 return _spec(self.chain[-1].ospec, "chainout")
234
235 def _sa_setup(self, m, i):
236 for (idx, c) in enumerate(self.chain):
237 if hasattr(c, "setup"):
238 c.setup(m, i) # stage may have some module stuff
239 ofn = self.chain[idx].ospec # last assignment survives
240 o = _spec(ofn, 'chainin%d' % idx)
241 m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
242 if idx == len(self.chain)-1:
243 break
244 ifn = self.chain[idx+1].ispec # new input on next loop
245 i = _spec(ifn, 'chainin%d' % (idx+1))
246 m.d.comb += nmoperator.eq(i, o) # assign to next input
247 self.o = o
248 return self.o # last loop is the output
249
250 def _na_setup(self, m, i):
251 for (idx, c) in enumerate(self.chain):
252 if hasattr(c, "setup"):
253 c.setup(m, i) # stage may have some module stuff
254 i = o = c.process(i) # store input into "o"
255 self.o = o
256 return self.o # last loop is the output
257
258 def process(self, i):
259 return self.o # conform to Stage API: return last-loop output
260
261