split stageapi into separate module, move ControlBase to singlepipe
authorLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Mon, 29 Apr 2019 03:49:11 +0000 (04:49 +0100)
committerLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Mon, 29 Apr 2019 03:49:11 +0000 (04:49 +0100)
src/add/iocontrol.py
src/add/singlepipe.py
src/add/stageapi.py [new file with mode: 0644]

index 81142c29921c0649a9692b851519165232316506..de1c938ea39efb5778c6cc69a32b0632be9ccc2c 100644 (file)
@@ -164,15 +164,6 @@ class RecordObject(Record):
         return list(self)
 
 
-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 PrevControl(Elaboratable):
     """ contains signals that come *from* the previous stage (both in and out)
         * valid_i: previous stage indicating all incoming data is valid.
@@ -312,315 +303,3 @@ class NextControl(Elaboratable):
     def ports(self):
         return list(self)
 
-
-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
-
-
-class ControlBase(StageHelper, Elaboratable):
-    """ Common functions for Pipeline API.  Note: a "pipeline stage" only
-        exists (conceptually) when a ControlBase derivative is handed
-        a Stage (combinatorial block)
-
-        NOTE: ControlBase derives from StageHelper, making it accidentally
-        compliant with the Stage API.  Using those functions directly
-        *BYPASSES* a ControlBase instance ready/valid signalling, which
-        clearly should not be done without a really, really good reason.
-    """
-    def __init__(self, stage=None, in_multi=None, stage_ctl=False):
-        """ Base class containing ready/valid/data to previous and next stages
-
-            * p: contains ready/valid to the previous stage
-            * n: contains ready/valid to the next stage
-
-            Except when calling Controlbase.connect(), user must also:
-            * add data_i member to PrevControl (p) and
-            * add data_o member to NextControl (n)
-            Calling ControlBase._new_data is a good way to do that.
-        """
-        StageHelper.__init__(self, stage)
-
-        # set up input and output IO ACK (prev/next ready/valid)
-        self.p = PrevControl(in_multi, stage_ctl)
-        self.n = NextControl(stage_ctl)
-
-        # set up the input and output data
-        if stage is not None:
-            self._new_data(self, self, "data")
-
-    def _new_data(self, p, n, name):
-        """ allocates new data_i and data_o
-        """
-        self.p.data_i = _spec(p.stage.ispec, "%s_i" % name)
-        self.n.data_o = _spec(n.stage.ospec, "%s_o" % name)
-
-    @property
-    def data_r(self):
-        return self.process(self.p.data_i)
-
-    def connect_to_next(self, nxt):
-        """ helper function to connect to the next stage data/valid/ready.
-        """
-        return self.n.connect_to_next(nxt.p)
-
-    def _connect_in(self, prev):
-        """ internal helper function to connect stage to an input source.
-            do not use to connect stage-to-stage!
-        """
-        return self.p._connect_in(prev.p)
-
-    def _connect_out(self, nxt):
-        """ internal helper function to connect stage to an output source.
-            do not use to connect stage-to-stage!
-        """
-        return self.n._connect_out(nxt.n)
-
-    def connect(self, pipechain):
-        """ connects a chain (list) of Pipeline instances together and
-            links them to this ControlBase instance:
-
-                      in <----> self <---> out
-                       |                   ^
-                       v                   |
-                    [pipe1, pipe2, pipe3, pipe4]
-                       |    ^  |    ^  |     ^
-                       v    |  v    |  v     |
-                     out---in out--in out---in
-
-            Also takes care of allocating data_i/data_o, by looking up
-            the data spec for each end of the pipechain.  i.e It is NOT
-            necessary to allocate self.p.data_i or self.n.data_o manually:
-            this is handled AUTOMATICALLY, here.
-
-            Basically this function is the direct equivalent of StageChain,
-            except that unlike StageChain, the Pipeline logic is followed.
-
-            Just as StageChain presents an object that conforms to the
-            Stage API from a list of objects that also conform to the
-            Stage API, an object that calls this Pipeline connect function
-            has the exact same pipeline API as the list of pipline objects
-            it is called with.
-
-            Thus it becomes possible to build up larger chains recursively.
-            More complex chains (multi-input, multi-output) will have to be
-            done manually.
-
-            Argument:
-
-            * :pipechain: - a sequence of ControlBase-derived classes
-                            (must be one or more in length)
-
-            Returns:
-
-            * a list of eq assignments that will need to be added in
-              an elaborate() to m.d.comb
-        """
-        assert len(pipechain) > 0, "pipechain must be non-zero length"
-        eqs = [] # collated list of assignment statements
-
-        # connect inter-chain
-        for i in range(len(pipechain)-1):
-            pipe1 = pipechain[i]                # earlier
-            pipe2 = pipechain[i+1]              # later (by 1)
-            eqs += pipe1.connect_to_next(pipe2) # earlier n to later p
-
-        # connect front and back of chain to ourselves
-        front = pipechain[0]                # first in chain
-        end = pipechain[-1]                 # last in chain
-        self._new_data(front, end, "chain") # NOTE: REPLACES existing data
-        eqs += front._connect_in(self)      # front p to our p
-        eqs += end._connect_out(self)       # end n   to out n
-
-        return eqs
-
-    def set_input(self, i):
-        """ helper function to set the input data (used in unit tests)
-        """
-        return nmoperator.eq(self.p.data_i, i)
-
-    def __iter__(self):
-        yield from self.p # yields ready/valid/data (data also gets yielded)
-        yield from self.n # ditto
-
-    def ports(self):
-        return list(self)
-
-    def elaborate(self, platform):
-        """ handles case where stage has dynamic ready/valid functions
-        """
-        m = Module()
-        m.submodules.p = self.p
-        m.submodules.n = self.n
-
-        self.setup(m, self.p.data_i)
-
-        if not self.p.stage_ctl:
-            return m
-
-        # intercept the previous (outgoing) "ready", combine with stage ready
-        m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready)
-
-        # intercept the next (incoming) "ready" and combine it with data valid
-        sdv = self.stage.d_valid(self.n.ready_i)
-        m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
-
-        return m
-
index 5808d8a9a730c8eed7c688384dd723812ce0060a..ebbb1e9b95401b074f73cb9d3d86ba789c6381c3 100644 (file)
     StageChain, however when passed to UnbufferedPipeline they
     can be used to introduce a single clock delay.
 
+    ControlBase:
+    -----------
+
+    The base class for pipelines.  Contains previous and next ready/valid/data.
+    Also has an extremely useful "connect" function that can be used to
+    connect a chain of pipelines and present the exact same prev/next
+    ready/valid/data API.
+
+    Note: pipelines basically do not become pipelines as such until
+    handed to a derivative of ControlBase.  ControlBase itself is *not*
+    strictly considered a pipeline class.  Wishbone and AXI4 (master or
+    slave) could be derived from ControlBase, for example.
     UnbufferedPipeline:
     ------------------
 
@@ -121,7 +133,7 @@ from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable
 from nmigen.cli import verilog, rtlil
 from nmigen.lib.fifo import SyncFIFO, SyncFIFOBuffered
 from nmigen.hdl.ast import ArrayProxy
-from nmigen.hdl.rec import Record, Layout
+from nmigen.hdl.rec import Record
 
 from abc import ABCMeta, abstractmethod
 from collections.abc import Sequence, Iterable
@@ -130,11 +142,161 @@ from queue import Queue
 import inspect
 
 import nmoperator
-from iocontrol import (Object, RecordObject, _spec,
-                       PrevControl, NextControl, StageCls, Stage,
-                       ControlBase, StageChain)
+from iocontrol import (Object, RecordObject)
+from stageapi import (_spec, PrevControl, NextControl, StageCls, Stage,
+                       StageChain, StageHelper)
                       
 
+
+class ControlBase(StageHelper, Elaboratable):
+    """ Common functions for Pipeline API.  Note: a "pipeline stage" only
+        exists (conceptually) when a ControlBase derivative is handed
+        a Stage (combinatorial block)
+
+        NOTE: ControlBase derives from StageHelper, making it accidentally
+        compliant with the Stage API.  Using those functions directly
+        *BYPASSES* a ControlBase instance ready/valid signalling, which
+        clearly should not be done without a really, really good reason.
+    """
+    def __init__(self, stage=None, in_multi=None, stage_ctl=False):
+        """ Base class containing ready/valid/data to previous and next stages
+
+            * p: contains ready/valid to the previous stage
+            * n: contains ready/valid to the next stage
+
+            Except when calling Controlbase.connect(), user must also:
+            * add data_i member to PrevControl (p) and
+            * add data_o member to NextControl (n)
+            Calling ControlBase._new_data is a good way to do that.
+        """
+        StageHelper.__init__(self, stage)
+
+        # set up input and output IO ACK (prev/next ready/valid)
+        self.p = PrevControl(in_multi, stage_ctl)
+        self.n = NextControl(stage_ctl)
+
+        # set up the input and output data
+        if stage is not None:
+            self._new_data(self, self, "data")
+
+    def _new_data(self, p, n, name):
+        """ allocates new data_i and data_o
+        """
+        self.p.data_i = _spec(p.stage.ispec, "%s_i" % name)
+        self.n.data_o = _spec(n.stage.ospec, "%s_o" % name)
+
+    @property
+    def data_r(self):
+        return self.process(self.p.data_i)
+
+    def connect_to_next(self, nxt):
+        """ helper function to connect to the next stage data/valid/ready.
+        """
+        return self.n.connect_to_next(nxt.p)
+
+    def _connect_in(self, prev):
+        """ internal helper function to connect stage to an input source.
+            do not use to connect stage-to-stage!
+        """
+        return self.p._connect_in(prev.p)
+
+    def _connect_out(self, nxt):
+        """ internal helper function to connect stage to an output source.
+            do not use to connect stage-to-stage!
+        """
+        return self.n._connect_out(nxt.n)
+
+    def connect(self, pipechain):
+        """ connects a chain (list) of Pipeline instances together and
+            links them to this ControlBase instance:
+
+                      in <----> self <---> out
+                       |                   ^
+                       v                   |
+                    [pipe1, pipe2, pipe3, pipe4]
+                       |    ^  |    ^  |     ^
+                       v    |  v    |  v     |
+                     out---in out--in out---in
+
+            Also takes care of allocating data_i/data_o, by looking up
+            the data spec for each end of the pipechain.  i.e It is NOT
+            necessary to allocate self.p.data_i or self.n.data_o manually:
+            this is handled AUTOMATICALLY, here.
+
+            Basically this function is the direct equivalent of StageChain,
+            except that unlike StageChain, the Pipeline logic is followed.
+
+            Just as StageChain presents an object that conforms to the
+            Stage API from a list of objects that also conform to the
+            Stage API, an object that calls this Pipeline connect function
+            has the exact same pipeline API as the list of pipline objects
+            it is called with.
+
+            Thus it becomes possible to build up larger chains recursively.
+            More complex chains (multi-input, multi-output) will have to be
+            done manually.
+
+            Argument:
+
+            * :pipechain: - a sequence of ControlBase-derived classes
+                            (must be one or more in length)
+
+            Returns:
+
+            * a list of eq assignments that will need to be added in
+              an elaborate() to m.d.comb
+        """
+        assert len(pipechain) > 0, "pipechain must be non-zero length"
+        eqs = [] # collated list of assignment statements
+
+        # connect inter-chain
+        for i in range(len(pipechain)-1):
+            pipe1 = pipechain[i]                # earlier
+            pipe2 = pipechain[i+1]              # later (by 1)
+            eqs += pipe1.connect_to_next(pipe2) # earlier n to later p
+
+        # connect front and back of chain to ourselves
+        front = pipechain[0]                # first in chain
+        end = pipechain[-1]                 # last in chain
+        self._new_data(front, end, "chain") # NOTE: REPLACES existing data
+        eqs += front._connect_in(self)      # front p to our p
+        eqs += end._connect_out(self)       # end n   to out n
+
+        return eqs
+
+    def set_input(self, i):
+        """ helper function to set the input data (used in unit tests)
+        """
+        return nmoperator.eq(self.p.data_i, i)
+
+    def __iter__(self):
+        yield from self.p # yields ready/valid/data (data also gets yielded)
+        yield from self.n # ditto
+
+    def ports(self):
+        return list(self)
+
+    def elaborate(self, platform):
+        """ handles case where stage has dynamic ready/valid functions
+        """
+        m = Module()
+        m.submodules.p = self.p
+        m.submodules.n = self.n
+
+        self.setup(m, self.p.data_i)
+
+        if not self.p.stage_ctl:
+            return m
+
+        # intercept the previous (outgoing) "ready", combine with stage ready
+        m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready)
+
+        # intercept the next (incoming) "ready" and combine it with data valid
+        sdv = self.stage.d_valid(self.n.ready_i)
+        m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
+
+        return m
+
 class RecordBasedStage(Stage):
     """ convenience class which provides a Records-based layout.
         honestly it's a lot easier just to create a direct Records-based
diff --git a/src/add/stageapi.py b/src/add/stageapi.py
new file mode 100644 (file)
index 0000000..9217c1f
--- /dev/null
@@ -0,0 +1,258 @@
+""" 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
+