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