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