--- /dev/null
+""" Stage API
+
+ Associated development bugs:
+ * http://bugs.libre-riscv.org/show_bug.cgi?id=64
+ * http://bugs.libre-riscv.org/show_bug.cgi?id=57
+
+ Stage API:
+ ---------
+
+ stage requires compliance with a strict API that may be
+ implemented in several means, including as a static class.
+
+ Stages do not HOLD data, and they definitely do not contain
+ signalling (ready/valid). They do however specify the FORMAT
+ of the incoming and outgoing data, and they provide a means to
+ PROCESS that data (from incoming format to outgoing format).
+
+ Stage Blocks really must be combinatorial blocks. It would be ok
+ to have input come in from sync'd sources (clock-driven) however by
+ doing so they would no longer be deterministic, and chaining such
+ blocks with such side-effects together could result in unexpected,
+ unpredictable, unreproduceable behaviour.
+ So generally to be avoided, then unless you know what you are doing.
+
+ the methods of a stage instance must be as follows:
+
+ * ispec() - Input data format specification. Takes a bit of explaining.
+ The requirements are: something that eventually derives from
+ nmigen Value must be returned *OR* an iterator or iterable
+ or sequence (list, tuple etc.) or generator must *yield*
+ thing(s) that (eventually) derive from the nmigen Value class.
+
+ Complex to state, very simple in practice:
+ see test_buf_pipe.py for over 25 worked examples.
+
+ * ospec() - Output data format specification.
+ format requirements identical to ispec.
+
+ * process(m, i) - Optional function for processing ispec-formatted data.
+ returns a combinatorial block of a result that
+ may be assigned to the output, by way of the "nmoperator.eq"
+ function. Note that what is returned here can be
+ extremely flexible. Even a dictionary can be returned
+ as long as it has fields that match precisely with the
+ Record into which its values is intended to be assigned.
+ Again: see example unit tests for details.
+
+ * setup(m, i) - Optional function for setting up submodules.
+ may be used for more complex stages, to link
+ the input (i) to submodules. must take responsibility
+ for adding those submodules to the module (m).
+ the submodules must be combinatorial blocks and
+ must have their inputs and output linked combinatorially.
+
+ Both StageCls (for use with non-static classes) and Stage (for use
+ by static classes) are abstract classes from which, for convenience
+ and as a courtesy to other developers, anything conforming to the
+ Stage API may *choose* to derive. See Liskov Substitution Principle:
+ https://en.wikipedia.org/wiki/Liskov_substitution_principle
+
+ StageChain:
+ ----------
+
+ A useful combinatorial wrapper around stages that chains them together
+ and then presents a Stage-API-conformant interface. By presenting
+ the same API as the stages it wraps, it can clearly be used recursively.
+
+ StageHelper:
+ ----------
+
+ A convenience wrapper around a Stage-API-compliant "thing" which
+ complies with the Stage API and provides mandatory versions of
+ all the optional bits.
+"""
+
+from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable
+from nmigen.cli import verilog, rtlil
+from nmigen.hdl.rec import Record
+
+from abc import ABCMeta, abstractmethod
+from collections.abc import Sequence, Iterable
+from collections import OrderedDict
+import inspect
+
+from iocontrol import PrevControl, NextControl
+import nmoperator
+
+
+def _spec(fn, name=None):
+ if name is None:
+ return fn()
+ varnames = dict(inspect.getmembers(fn.__code__))['co_varnames']
+ if 'name' in varnames:
+ return fn(name=name)
+ return fn()
+
+
+class StageCls(metaclass=ABCMeta):
+ """ Class-based "Stage" API. requires instantiation (after derivation)
+
+ see "Stage API" above.. Note: python does *not* require derivation
+ from this class. All that is required is that the pipelines *have*
+ the functions listed in this class. Derivation from this class
+ is therefore merely a "courtesy" to maintainers.
+ """
+ @abstractmethod
+ def ispec(self): pass # REQUIRED
+ @abstractmethod
+ def ospec(self): pass # REQUIRED
+ #@abstractmethod
+ #def setup(self, m, i): pass # OPTIONAL
+ #@abstractmethod
+ #def process(self, i): pass # OPTIONAL
+
+
+class Stage(metaclass=ABCMeta):
+ """ Static "Stage" API. does not require instantiation (after derivation)
+
+ see "Stage API" above. Note: python does *not* require derivation
+ from this class. All that is required is that the pipelines *have*
+ the functions listed in this class. Derivation from this class
+ is therefore merely a "courtesy" to maintainers.
+ """
+ @staticmethod
+ @abstractmethod
+ def ispec(): pass
+
+ @staticmethod
+ @abstractmethod
+ def ospec(): pass
+
+ #@staticmethod
+ #@abstractmethod
+ #def setup(m, i): pass
+
+ #@staticmethod
+ #@abstractmethod
+ #def process(i): pass
+
+
+class StageChain(StageCls):
+ """ pass in a list of stages, and they will automatically be
+ chained together via their input and output specs into a
+ combinatorial chain, to create one giant combinatorial block.
+
+ the end result basically conforms to the exact same Stage API.
+
+ * input to this class will be the input of the first stage
+ * output of first stage goes into input of second
+ * output of second goes into input into third
+ * ... (etc. etc.)
+ * the output of this class will be the output of the last stage
+
+ NOTE: whilst this is very similar to ControlBase.connect(), it is
+ *really* important to appreciate that StageChain is pure
+ combinatorial and bypasses (does not involve, at all, ready/valid
+ signalling of any kind).
+
+ ControlBase.connect on the other hand respects, connects, and uses
+ ready/valid signalling.
+
+ Arguments:
+
+ * :chain: a chain of combinatorial blocks conforming to the Stage API
+ NOTE: StageChain.ispec and ospect have to have something
+ to return (beginning and end specs of the chain),
+ therefore the chain argument must be non-zero length
+
+ * :specallocate: if set, new input and output data will be allocated
+ and connected (eq'd) to each chained Stage.
+ in some cases if this is not done, the nmigen warning
+ "driving from two sources, module is being flattened"
+ will be issued.
+
+ NOTE: do NOT use StageChain with combinatorial blocks that have
+ side-effects (state-based / clock-based input) or conditional
+ (inter-chain) dependencies, unless you really know what you are doing.
+ """
+ def __init__(self, chain, specallocate=False):
+ assert len(chain) > 0, "stage chain must be non-zero length"
+ self.chain = chain
+ self.specallocate = specallocate
+
+ def ispec(self):
+ """ returns the ispec of the first of the chain
+ """
+ return _spec(self.chain[0].ispec, "chainin")
+
+ def ospec(self):
+ """ returns the ospec of the last of the chain
+ """
+ return _spec(self.chain[-1].ospec, "chainout")
+
+ def _specallocate_setup(self, m, i):
+ for (idx, c) in enumerate(self.chain):
+ if hasattr(c, "setup"):
+ c.setup(m, i) # stage may have some module stuff
+ ofn = self.chain[idx].ospec # last assignment survives
+ o = _spec(ofn, 'chainin%d' % idx)
+ m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
+ if idx == len(self.chain)-1:
+ break
+ ifn = self.chain[idx+1].ispec # new input on next loop
+ i = _spec(ifn, 'chainin%d' % (idx+1))
+ m.d.comb += nmoperator.eq(i, o) # assign to next input
+ return o # last loop is the output
+
+ def _noallocate_setup(self, m, i):
+ for (idx, c) in enumerate(self.chain):
+ if hasattr(c, "setup"):
+ c.setup(m, i) # stage may have some module stuff
+ i = o = c.process(i) # store input into "o"
+ return o # last loop is the output
+
+ def setup(self, m, i):
+ if self.specallocate:
+ self.o = self._specallocate_setup(m, i)
+ else:
+ self.o = self._noallocate_setup(m, i)
+
+ def process(self, i):
+ return self.o # conform to Stage API: return last-loop output
+
+
+class StageHelper(Stage):
+ """ a convenience wrapper around something that is Stage-API-compliant.
+ (that "something" may be a static class, for example).
+
+ StageHelper happens to also be compliant with the Stage API,
+ except that all the "optional" functions are provided
+ (hence the designation "convenience wrapper")
+ """
+ def __init__(self, stage):
+ self.stage = stage
+
+ def ospec(self, name):
+ assert self.stage is not None
+ return _spec(self.stage.ospec, name)
+
+ def ispec(self, name):
+ assert self.stage is not None
+ return _spec(self.stage.ispec, name)
+
+ def process(self, i):
+ if self.stage and hasattr(self.stage, "process"):
+ return self.stage.process(i)
+ return i
+
+ def setup(self, m, i):
+ if self.stage is not None and hasattr(self.stage, "setup"):
+ self.stage.setup(m, i)
+
+ def _postprocess(self, i): # XXX DISABLED
+ return i # RETURNS INPUT
+ if hasattr(self.stage, "postprocess"):
+ return self.stage.postprocess(i)
+ return i
+