update comments
[ieee754fpu.git] / src / add / singlepipe.py
index 95ca82a469c9a20ed0593e895249672ad4d93291..68b62e432d4fc6022b99361c3358d01638414fce 100644 (file)
@@ -1,50 +1,10 @@
-""" Pipeline and BufferedHandshake implementation, conforming to the same API.
-    For multi-input and multi-output variants, see multipipe.
-
-    eq:
-    --
-
-    a strategically very important function that is identical in function
-    to nmigen's Signal.eq function, except it may take objects, or a list
-    of objects, or a tuple of objects, and where objects may also be
-    Records.
-
-    Stage API:
-    ---------
-
-    stage requires compliance with a strict API that may be
-    implemented in several means, including as a static class.
-    the methods of a stage instance must be as follows:
-
-    * ispec() - Input data format specification
-                returns an object or a list or tuple of objects, or
-                a Record, each object having an "eq" function which
-                takes responsibility for copying by assignment all
-                sub-objects
-    * ospec() - Output data format specification
-                requirements as for ospec
-    * process(m, i) - Processes an ispec-formatted object
-                returns a combinatorial block of a result that
-                may be assigned to the output, by way of the "eq"
-                function
-    * 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.
-
-    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.
+""" Pipeline API.  For multi-input and multi-output variants, see multipipe.
+
+    Associated development bugs:
+    * http://bugs.libre-riscv.org/show_bug.cgi?id=64
+    * http://bugs.libre-riscv.org/show_bug.cgi?id=57
+
+    Important: see Stage API (stageapi.py) in combination with below
 
     RecordBasedStage:
     ----------------
     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:
     ------------------
 
     where data will flow on *every* clock when the conditions are right.
 
     input acceptance conditions are when:
-        * incoming previous-stage strobe (p.i_valid) is HIGH
-        * outgoing previous-stage ready   (p.o_ready) is LOW
+        * incoming previous-stage strobe (p.valid_i) is HIGH
+        * outgoing previous-stage ready   (p.ready_o) is LOW
 
     output transmission conditions are when:
-        * outgoing next-stage strobe (n.o_valid) is HIGH
-        * outgoing next-stage ready   (n.i_ready) is LOW
+        * outgoing next-stage strobe (n.valid_o) is HIGH
+        * outgoing next-stage ready   (n.ready_i) is LOW
 
     the tricky bit is when the input has valid data and the output is not
     ready to accept it.  if it wasn't for the clock synchronisation, it
     https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v
 """
 
-from nmigen import Signal, Cat, Const, Mux, Module, Value
+from nmigen import Signal, Mux, Module, Elaboratable
 from nmigen.cli import verilog, rtlil
-from nmigen.lib.fifo import SyncFIFO
-from nmigen.hdl.ast import ArrayProxy
-from nmigen.hdl.rec import Record, Layout
-
-from abc import ABCMeta, abstractmethod
-from collections.abc import Sequence
-
-
-class RecordObject(Record):
-    def __init__(self, layout=None, name=None):
-        Record.__init__(self, layout=layout or [], name=None)
-
-    def __setattr__(self, k, v):
-        if k in dir(Record) or "fields" not in self.__dict__:
-            return object.__setattr__(self, k, v)
-        self.fields[k] = v
-        if isinstance(v, Record):
-            newlayout = {k: (k, v.layout)}
-        else:
-            newlayout = {k: (k, v.shape())}
-        self.layout.fields.update(newlayout)
-
-
-
-class PrevControl:
-    """ contains signals that come *from* the previous stage (both in and out)
-        * i_valid: previous stage indicating all incoming data is valid.
-                   may be a multi-bit signal, where all bits are required
-                   to be asserted to indicate "valid".
-        * o_ready: output to next stage indicating readiness to accept data
-        * i_data : an input - added by the user of this class
-    """
-
-    def __init__(self, i_width=1, stage_ctl=False):
-        self.stage_ctl = stage_ctl
-        self.i_valid = Signal(i_width, name="p_i_valid") # prev   >>in  self
-        self._o_ready = Signal(name="p_o_ready") # prev   <<out self
-        self.i_data = None # XXX MUST BE ADDED BY USER
-        if stage_ctl:
-            self.s_o_ready = Signal(name="p_s_o_rdy") # prev   <<out self
-
-    @property
-    def o_ready(self):
-        """ public-facing API: indicates (externally) that stage is ready
-        """
-        if self.stage_ctl:
-            return self.s_o_ready # set dynamically by stage
-        return self._o_ready      # return this when not under dynamic control
-
-    def _connect_in(self, prev, direct=False, fn=None):
-        """ internal helper function to connect stage to an input source.
-            do not use to connect stage-to-stage!
-        """
-        i_valid = prev.i_valid if direct else prev.i_valid_test
-        i_data = fn(prev.i_data) if fn is not None else prev.i_data
-        return [self.i_valid.eq(i_valid),
-                prev.o_ready.eq(self.o_ready),
-                eq(self.i_data, i_data),
-               ]
-
-    @property
-    def i_valid_test(self):
-        vlen = len(self.i_valid)
-        if vlen > 1:
-            # multi-bit case: valid only when i_valid is all 1s
-            all1s = Const(-1, (len(self.i_valid), False))
-            i_valid = (self.i_valid == all1s)
-        else:
-            # single-bit i_valid case
-            i_valid = self.i_valid
-
-        # when stage indicates not ready, incoming data
-        # must "appear" to be not ready too
-        if self.stage_ctl:
-            i_valid = i_valid & self.s_o_ready
-
-        return i_valid
-
-
-class NextControl:
-    """ contains the signals that go *to* the next stage (both in and out)
-        * o_valid: output indicating to next stage that data is valid
-        * i_ready: input from next stage indicating that it can accept data
-        * o_data : an output - added by the user of this class
-    """
-    def __init__(self, stage_ctl=False):
-        self.stage_ctl = stage_ctl
-        self.o_valid = Signal(name="n_o_valid") # self out>>  next
-        self.i_ready = Signal(name="n_i_ready") # self <<in   next
-        self.o_data = None # XXX MUST BE ADDED BY USER
-        #if self.stage_ctl:
-        self.d_valid = Signal(reset=1) # INTERNAL (data valid)
-
-    @property
-    def i_ready_test(self):
-        if self.stage_ctl:
-            return self.i_ready & self.d_valid
-        return self.i_ready
-
-    def connect_to_next(self, nxt):
-        """ helper function to connect to the next stage data/valid/ready.
-            data/valid is passed *TO* nxt, and ready comes *IN* from nxt.
-            use this when connecting stage-to-stage
-        """
-        return [nxt.i_valid.eq(self.o_valid),
-                self.i_ready.eq(nxt.o_ready),
-                eq(nxt.i_data, self.o_data),
-               ]
-
-    def _connect_out(self, nxt, direct=False, fn=None):
-        """ internal helper function to connect stage to an output source.
-            do not use to connect stage-to-stage!
-        """
-        i_ready = nxt.i_ready if direct else nxt.i_ready_test
-        o_data = fn(nxt.o_data) if fn is not None else nxt.o_data
-        return [nxt.o_valid.eq(self.o_valid),
-                self.i_ready.eq(i_ready),
-                eq(o_data, self.o_data),
-               ]
-
-
-class Visitor:
-    """ a helper routine which identifies if it is being passed a list
-        (or tuple) of objects, or signals, or Records, and calls
-        a visitor function.
-
-        the visiting fn is called when an object is identified.
-
-        Record is a special (unusual, recursive) case, where the input may be
-        specified as a dictionary (which may contain further dictionaries,
-        recursively), where the field names of the dictionary must match
-        the Record's field spec.  Alternatively, an object with the same
-        member names as the Record may be assigned: it does not have to
-        *be* a Record.
-
-        ArrayProxy is also special-cased, it's a bit messy: whilst ArrayProxy
-        has an eq function, the object being assigned to it (e.g. a python
-        object) might not.  despite the *input* having an eq function,
-        that doesn't help us, because it's the *ArrayProxy* that's being
-        assigned to.  so.... we cheat.  use the ports() function of the
-        python object, enumerate them, find out the list of Signals that way,
-        and assign them.
-    """
-    def visit(self, o, i, act):
-        if isinstance(o, dict):
-            return self.dict_visit(o, i, act)
-
-        res = act.prepare()
-        if not isinstance(o, Sequence):
-            o, i = [o], [i]
-        for (ao, ai) in zip(o, i):
-            #print ("visit", fn, ao, ai)
-            if isinstance(ao, Record):
-                rres = self.record_visit(ao, ai, act)
-            elif isinstance(ao, ArrayProxy) and not isinstance(ai, Value):
-                rres = self.arrayproxy_visit(ao, ai, act)
-            else:
-                rres = act.fn(ao, ai)
-            res += rres
-        return res
-
-    def dict_visit(self, o, i, act):
-        res = act.prepare()
-        for (k, v) in o.items():
-            print ("d-eq", v, i[k])
-            res.append(act.fn(v, i[k]))
-        return res
-
-    def record_visit(self, ao, ai, act):
-        res = act.prepare()
-        for idx, (field_name, field_shape, _) in enumerate(ao.layout):
-            if isinstance(field_shape, Layout):
-                val = ai.fields
-            else:
-                val = ai
-            if hasattr(val, field_name): # check for attribute
-                val = getattr(val, field_name)
-            else:
-                val = val[field_name] # dictionary-style specification
-            val = self.visit(ao.fields[field_name], val, act)
-            if isinstance(val, Sequence):
-                res += val
-            else:
-                res.append(val)
-        return res
-
-    def arrayproxy_visit(self, ao, ai, act):
-        res = act.prepare()
-        for p in ai.ports():
-            op = getattr(ao, p.name)
-            #print (op, p, p.name)
-            res.append(fn(op, p))
-        return res
-
-
-class Eq(Visitor):
-    def __init__(self):
-        self.res = []
-    def prepare(self):
-        return []
-    def fn(self, o, i):
-        rres = o.eq(i)
-        if not isinstance(rres, Sequence):
-            rres = [rres]
-        return rres
-    def __call__(self, o, i):
-        return self.visit(o, i, self)
-
-
-def eq(o, i):
-    """ makes signals equal: a helper routine which identifies if it is being
-        passed a list (or tuple) of objects, or signals, or Records, and calls
-        the objects' eq function.
-    """
-    return Eq()(o, i)
-
-
-def flatten(i):
-    """ flattens a compound structure recursively using Cat
-    """
-    if not isinstance(i, Sequence):
-        i = [i]
-    res = []
-    for ai in i:
-        print ("flatten", ai)
-        if isinstance(ai, Record):
-            print ("record", list(ai.layout))
-            rres = []
-            for idx, (field_name, field_shape, _) in enumerate(ai.layout):
-                if isinstance(field_shape, Layout):
-                    val = ai.fields
-                else:
-                    val = ai
-                if hasattr(val, field_name): # check for attribute
-                    val = getattr(val, field_name)
-                else:
-                    val = val[field_name] # dictionary-style specification
-                print ("recidx", idx, field_name, field_shape, val)
-                val = flatten(val)
-                print ("recidx flat", idx, val)
-                if isinstance(val, Sequence):
-                    rres += val
-                else:
-                    rres.append(val)
-
-        elif isinstance(ai, ArrayProxy) and not isinstance(ai, Value):
-            rres = []
-            for p in ai.ports():
-                op = getattr(ai, p.name)
-                #print (op, p, p.name)
-                rres.append(flatten(p))
-        else:
-            rres = ai
-        if not isinstance(rres, Sequence):
-            rres = [rres]
-        res += rres
-        print ("flatten res", res)
-    return Cat(*res)
+from nmigen.hdl.rec import Record
 
+from queue import Queue
+import inspect
 
-
-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  # REQUIRED
-
-
-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
-
+from iocontrol import (PrevControl, NextControl, Object, RecordObject)
+from stageapi import (_spec, StageCls, Stage, StageChain, StageHelper)
+import nmoperator
+                      
 
 class RecordBasedStage(Stage):
     """ convenience class which provides a Records-based layout.
@@ -486,59 +156,28 @@ class RecordBasedStage(Stage):
     def setup(seif, m, i): return self.__setup(m, i)
 
 
-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.
-
-        the end result basically conforms to the exact same Stage API.
+class PassThroughStage(StageCls):
+    """ a pass-through stage with its input data spec identical to its output,
+        and "passes through" its data from input to output (does nothing).
 
-        * 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
+        use this basically to explicitly make any data spec Stage-compliant.
+        (many APIs would potentially use a static "wrap" method in e.g.
+         StageCls to achieve a similar effect)
     """
-    def __init__(self, chain, specallocate=False):
-        self.chain = chain
-        self.specallocate = specallocate
-
-    def ispec(self):
-        return self.chain[0].ispec()
-
-    def ospec(self):
-        return self.chain[-1].ospec()
-
-    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
-            o = self.chain[idx].ospec()     # last assignment survives
-            m.d.comb += eq(o, c.process(i)) # process input into "o"
-            if idx == len(self.chain)-1:
-                break
-            i = self.chain[idx+1].ispec()   # new input on next loop
-            m.d.comb += 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 __init__(self, iospecfn): self.iospecfn = iospecfn
+    def ispec(self): return self.iospecfn()
+    def ospec(self): return self.iospecfn()
 
-    def process(self, i):
-        return self.o # conform to Stage API: return last-loop output
 
+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)
 
-class ControlBase:
-    """ Common functions for Pipeline API
+        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
@@ -547,10 +186,11 @@ class ControlBase:
             * n: contains ready/valid to the next stage
 
             Except when calling Controlbase.connect(), user must also:
-            * add i_data member to PrevControl (p) and
-            * add o_data member to NextControl (n)
+            * 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.
         """
-        self.stage = stage
+        StageHelper.__init__(self, stage)
 
         # set up input and output IO ACK (prev/next ready/valid)
         self.p = PrevControl(in_multi, stage_ctl)
@@ -558,8 +198,16 @@ class ControlBase:
 
         # set up the input and output data
         if stage is not None:
-            self.p.i_data = stage.ispec() # input type
-            self.n.o_data = stage.ospec()
+            self._new_data("data")
+
+    def _new_data(self, name):
+        """ allocates new data_i and data_o
+        """
+        self.p.data_i, self.n.data_o = self.new_specs(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.
@@ -590,9 +238,9 @@ class ControlBase:
                        v    |  v    |  v     |
                      out---in out--in out---in
 
-            Also takes care of allocating i_data/o_data, by looking up
+            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.i_data or self.n.o_data manually:
+            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,
@@ -607,63 +255,67 @@ class ControlBase:
             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"
+        assert self.stage is None, "do not use connect with a stage"
         eqs = [] # collated list of assignment statements
 
         # connect inter-chain
         for i in range(len(pipechain)-1):
-            pipe1 = pipechain[i]
-            pipe2 = pipechain[i+1]
-            eqs += pipe1.connect_to_next(pipe2)
-
-        # connect front of chain to ourselves
-        front = pipechain[0]
-        self.p.i_data = front.stage.ispec()
-        eqs += front._connect_in(self)
-
-        # connect end of chain to ourselves
-        end = pipechain[-1]
-        self.n.o_data = end.stage.ospec()
-        eqs += end._connect_out(self)
+            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.set_specs(front, end) # sets up ispec/ospec functions
+        self._new_data("chain") # NOTE: REPLACES existing data
+        eqs += front._connect_in(self)      # front p to our p
+        eqs += end._connect_out(self)       # end n   to our n
 
         return eqs
 
     def set_input(self, i):
-        """ helper function to set the input data
+        """ helper function to set the input data (used in unit tests)
         """
-        return eq(self.p.i_data, i)
+        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):
-        res = [self.p.i_valid, self.n.i_ready,
-                self.n.o_valid, self.p.o_ready,
-               ]
-        if hasattr(self.p.i_data, "ports"):
-            res += self.p.i_data.ports()
-        else:
-            res += self.p.i_data
-        if hasattr(self.n.o_data, "ports"):
-            res += self.n.o_data.ports()
-        else:
-            res += self.n.o_data
-        return res
+        return list(self)
 
-    def _elaborate(self, platform):
+    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
 
-        if self.stage is not None and hasattr(self.stage, "setup"):
-            self.stage.setup(m, self.p.i_data)
+        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_o_ready.eq(self.p._o_ready & self.stage.d_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.i_ready)
-        m.d.comb += self.n.d_valid.eq(self.n.i_ready & sdv)
+        sdv = self.stage.d_valid(self.n.ready_i)
+        m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
 
         return m
 
@@ -675,15 +327,15 @@ class BufferedHandshake(ControlBase):
 
         Argument: stage.  see Stage API above
 
-        stage-1   p.i_valid >>in   stage   n.o_valid out>>   stage+1
-        stage-1   p.o_ready <<out  stage   n.i_ready <<in    stage+1
-        stage-1   p.i_data  >>in   stage   n.o_data  out>>   stage+1
+        stage-1   p.valid_i >>in   stage   n.valid_o out>>   stage+1
+        stage-1   p.ready_o <<out  stage   n.ready_i <<in    stage+1
+        stage-1   p.data_i  >>in   stage   n.data_o  out>>   stage+1
                               |             |
                             process --->----^
                               |             |
                               +-- r_data ->-+
 
-        input data p.i_data is read (only), is processed and goes into an
+        input data p.data_i is read (only), is processed and goes into an
         intermediate result store [process()].  this is updated combinatorially.
 
         in a non-stall condition, the intermediate result will go into the
@@ -699,52 +351,54 @@ class BufferedHandshake(ControlBase):
     """
 
     def elaborate(self, platform):
-        self.m = ControlBase._elaborate(self, platform)
+        self.m = ControlBase.elaborate(self, platform)
 
-        result = self.stage.ospec()
-        r_data = self.stage.ospec()
+        result = _spec(self.stage.ospec, "r_tmp")
+        r_data = _spec(self.stage.ospec, "r_data")
 
         # establish some combinatorial temporaries
         o_n_validn = Signal(reset_less=True)
-        n_i_ready = Signal(reset_less=True, name="n_i_rdy_data")
+        n_ready_i = Signal(reset_less=True, name="n_i_rdy_data")
         nir_por = Signal(reset_less=True)
         nir_por_n = Signal(reset_less=True)
-        p_i_valid = Signal(reset_less=True)
+        p_valid_i = Signal(reset_less=True)
         nir_novn = Signal(reset_less=True)
         nirn_novn = Signal(reset_less=True)
         por_pivn = Signal(reset_less=True)
         npnn = Signal(reset_less=True)
-        self.m.d.comb += [p_i_valid.eq(self.p.i_valid_test),
-                     o_n_validn.eq(~self.n.o_valid),
-                     n_i_ready.eq(self.n.i_ready_test),
-                     nir_por.eq(n_i_ready & self.p._o_ready),
-                     nir_por_n.eq(n_i_ready & ~self.p._o_ready),
-                     nir_novn.eq(n_i_ready | o_n_validn),
-                     nirn_novn.eq(~n_i_ready & o_n_validn),
+        self.m.d.comb += [p_valid_i.eq(self.p.valid_i_test),
+                     o_n_validn.eq(~self.n.valid_o),
+                     n_ready_i.eq(self.n.ready_i_test),
+                     nir_por.eq(n_ready_i & self.p._ready_o),
+                     nir_por_n.eq(n_ready_i & ~self.p._ready_o),
+                     nir_novn.eq(n_ready_i | o_n_validn),
+                     nirn_novn.eq(~n_ready_i & o_n_validn),
                      npnn.eq(nir_por | nirn_novn),
-                     por_pivn.eq(self.p._o_ready & ~p_i_valid)
+                     por_pivn.eq(self.p._ready_o & ~p_valid_i)
         ]
 
         # store result of processing in combinatorial temporary
-        self.m.d.comb += eq(result, self.stage.process(self.p.i_data))
+        self.m.d.comb += nmoperator.eq(result, self.data_r)
 
         # if not in stall condition, update the temporary register
-        with self.m.If(self.p.o_ready): # not stalled
-            self.m.d.sync += eq(r_data, result) # update buffer
+        with self.m.If(self.p.ready_o): # not stalled
+            self.m.d.sync += nmoperator.eq(r_data, result) # update buffer
 
         # data pass-through conditions
         with self.m.If(npnn):
-            self.m.d.sync += [self.n.o_valid.eq(p_i_valid), # valid if p_valid
-                              eq(self.n.o_data, result),    # update output
+            data_o = self._postprocess(result) # XXX TBD, does nothing right now
+            self.m.d.sync += [self.n.valid_o.eq(p_valid_i), # valid if p_valid
+                              nmoperator.eq(self.n.data_o, data_o), # update out
                              ]
         # buffer flush conditions (NOTE: can override data passthru conditions)
         with self.m.If(nir_por_n): # not stalled
             # Flush the [already processed] buffer to the output port.
-            self.m.d.sync += [self.n.o_valid.eq(1),  # reg empty
-                              eq(self.n.o_data, r_data), # flush buffer
+            data_o = self._postprocess(r_data) # XXX TBD, does nothing right now
+            self.m.d.sync += [self.n.valid_o.eq(1),  # reg empty
+                              nmoperator.eq(self.n.data_o, data_o), # flush
                              ]
         # output ready conditions
-        self.m.d.sync += self.p._o_ready.eq(nir_novn | por_pivn)
+        self.m.d.sync += self.p._ready_o.eq(nir_novn | por_pivn)
 
         return self.m
 
@@ -755,75 +409,77 @@ class SimpleHandshake(ControlBase):
 
         Argument: stage.  see Stage API above
 
-        stage-1   p.i_valid >>in   stage   n.o_valid out>>   stage+1
-        stage-1   p.o_ready <<out  stage   n.i_ready <<in    stage+1
-        stage-1   p.i_data  >>in   stage   n.o_data  out>>   stage+1
+        stage-1   p.valid_i >>in   stage   n.valid_o out>>   stage+1
+        stage-1   p.ready_o <<out  stage   n.ready_i <<in    stage+1
+        stage-1   p.data_i  >>in   stage   n.data_o  out>>   stage+1
                               |             |
                               +--process->--^
         Truth Table
 
-        Inputs   Temporary  Output
-        -------  ---------- -----
-        P P N N  PiV& ~NiV&  N P
+        Inputs   Temporary  Output Data
+        -------  ---------- -----  ----
+        P P N N  PiV& ~NiR&  N P
         i o i o  PoR  NoV    o o
         V R R V              V R
 
         -------   -    -     - -
-        0 0 0 0   0    0    >0 0
-        0 0 0 1   0    1    >1 0
-        0 0 1 0   0    0     0 1
-        0 0 1 1   0    0     0 1
+        0 0 0 0   0    0    >0 0    reg
+        0 0 0 1   0    1    >1 0    reg
+        0 0 1 0   0    0     0 1    process(data_i)
+        0 0 1 1   0    0     0 1    process(data_i)
         -------   -    -     - -
-        0 1 0 0   0    0    >0 0
-        0 1 0 1   0    1    >1 0
-        0 1 1 0   0    0     0 1
-        0 1 1 1   0    0     0 1
+        0 1 0 0   0    0    >0 0    reg
+        0 1 0 1   0    1    >1 0    reg
+        0 1 1 0   0    0     0 1    process(data_i)
+        0 1 1 1   0    0     0 1    process(data_i)
         -------   -    -     - -
-        1 0 0 0   0    0    >0 0
-        1 0 0 1   0    1    >1 0
-        1 0 1 0   0    0     0 1
-        1 0 1 1   0    0     0 1
+        1 0 0 0   0    0    >0 0    reg
+        1 0 0 1   0    1    >1 0    reg
+        1 0 1 0   0    0     0 1    process(data_i)
+        1 0 1 1   0    0     0 1    process(data_i)
         -------   -    -     - -
-        1 1 0 0   1    0     1 0
-        1 1 0 1   1    1     1 0
-        1 1 1 0   1    0     1 1
-        1 1 1 1   1    0     1 1
+        1 1 0 0   1    0     1 0    process(data_i)
+        1 1 0 1   1    1     1 0    process(data_i)
+        1 1 1 0   1    0     1 1    process(data_i)
+        1 1 1 1   1    0     1 1    process(data_i)
         -------   -    -     - -
     """
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
 
         r_busy = Signal()
-        result = self.stage.ospec()
+        result = _spec(self.stage.ospec, "r_tmp")
 
         # establish some combinatorial temporaries
-        n_i_ready = Signal(reset_less=True, name="n_i_rdy_data")
-        p_i_valid_p_o_ready = Signal(reset_less=True)
-        p_i_valid = Signal(reset_less=True)
-        m.d.comb += [p_i_valid.eq(self.p.i_valid_test),
-                     n_i_ready.eq(self.n.i_ready_test),
-                     p_i_valid_p_o_ready.eq(p_i_valid & self.p.o_ready),
+        n_ready_i = Signal(reset_less=True, name="n_i_rdy_data")
+        p_valid_i_p_ready_o = Signal(reset_less=True)
+        p_valid_i = Signal(reset_less=True)
+        m.d.comb += [p_valid_i.eq(self.p.valid_i_test),
+                     n_ready_i.eq(self.n.ready_i_test),
+                     p_valid_i_p_ready_o.eq(p_valid_i & self.p.ready_o),
         ]
 
         # store result of processing in combinatorial temporary
-        m.d.comb += eq(result, self.stage.process(self.p.i_data))
+        m.d.comb += nmoperator.eq(result, self.data_r)
 
         # previous valid and ready
-        with m.If(p_i_valid_p_o_ready):
+        with m.If(p_valid_i_p_ready_o):
+            data_o = self._postprocess(result) # XXX TBD, does nothing right now
             m.d.sync += [r_busy.eq(1),      # output valid
-                         eq(self.n.o_data, result), # update output
+                         nmoperator.eq(self.n.data_o, data_o), # update output
                         ]
         # previous invalid or not ready, however next is accepting
-        with m.Elif(n_i_ready):
-            m.d.sync += [eq(self.n.o_data, result)]
+        with m.Elif(n_ready_i):
+            data_o = self._postprocess(result) # XXX TBD, does nothing right now
+            m.d.sync += [nmoperator.eq(self.n.data_o, data_o)]
             # TODO: could still send data here (if there was any)
-            #m.d.sync += self.n.o_valid.eq(0) # ...so set output invalid
+            #m.d.sync += self.n.valid_o.eq(0) # ...so set output invalid
             m.d.sync += r_busy.eq(0) # ...so set output invalid
 
-        m.d.comb += self.n.o_valid.eq(r_busy)
+        m.d.comb += self.n.valid_o.eq(r_busy)
         # if next is ready, so is previous
-        m.d.comb += self.p._o_ready.eq(n_i_ready)
+        m.d.comb += self.p._ready_o.eq(n_ready_i)
 
         return self.m
 
@@ -842,9 +498,9 @@ class UnbufferedPipeline(ControlBase):
 
         Argument: stage.  see Stage API, above
 
-        stage-1   p.i_valid >>in   stage   n.o_valid out>>   stage+1
-        stage-1   p.o_ready <<out  stage   n.i_ready <<in    stage+1
-        stage-1   p.i_data  >>in   stage   n.o_data  out>>   stage+1
+        stage-1   p.valid_i >>in   stage   n.valid_o out>>   stage+1
+        stage-1   p.ready_o <<out  stage   n.ready_i <<in    stage+1
+        stage-1   p.data_i  >>in   stage   n.data_o  out>>   stage+1
                               |             |
                             r_data        result
                               |             |
@@ -852,9 +508,9 @@ class UnbufferedPipeline(ControlBase):
 
         Attributes:
         -----------
-        p.i_data : StageInput, shaped according to ispec
+        p.data_i : StageInput, shaped according to ispec
             The pipeline input
-        p.o_data : StageOutput, shaped according to ospec
+        p.data_o : StageOutput, shaped according to ospec
             The pipeline output
         r_data : input_shape according to ispec
             A temporary (buffered) copy of a prior (valid) input.
@@ -866,60 +522,62 @@ class UnbufferedPipeline(ControlBase):
 
         Truth Table
 
-        Inputs  Temp  Output
-        -------   -   -----
+        Inputs  Temp  Output  Data
+        -------   -   -----   ----
         P P N N ~NiR&  N P
         i o i o  NoV   o o
         V R R V        V R
 
         -------   -    - -
-        0 0 0 0   0    0 1
-        0 0 0 1   1    1 0
-        0 0 1 0   0    0 1
-        0 0 1 1   0    0 1
+        0 0 0 0   0    0 1    reg
+        0 0 0 1   1    1 0    reg
+        0 0 1 0   0    0 1    reg
+        0 0 1 1   0    0 1    reg
         -------   -    - -
-        0 1 0 0   0    0 1
-        0 1 0 1   1    1 0
-        0 1 1 0   0    0 1
-        0 1 1 1   0    0 1
+        0 1 0 0   0    0 1    reg
+        0 1 0 1   1    1 0    reg
+        0 1 1 0   0    0 1    reg
+        0 1 1 1   0    0 1    reg
         -------   -    - -
-        1 0 0 0   0    1 1
-        1 0 0 1   1    1 0
-        1 0 1 0   0    1 1
-        1 0 1 1   0    1 1
+        1 0 0 0   0    1 1    reg
+        1 0 0 1   1    1 0    reg
+        1 0 1 0   0    1 1    reg
+        1 0 1 1   0    1 1    reg
         -------   -    - -
-        1 1 0 0   0    1 1
-        1 1 0 1   1    1 0
-        1 1 1 0   0    1 1
-        1 1 1 1   0    1 1
+        1 1 0 0   0    1 1    process(data_i)
+        1 1 0 1   1    1 0    process(data_i)
+        1 1 1 0   0    1 1    process(data_i)
+        1 1 1 1   0    1 1    process(data_i)
         -------   -    - -
 
         Note: PoR is *NOT* involved in the above decision-making.
     """
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
 
         data_valid = Signal() # is data valid or not
-        r_data = self.stage.ospec() # output type
+        r_data = _spec(self.stage.ospec, "r_tmp") # output type
 
         # some temporaries
-        p_i_valid = Signal(reset_less=True)
+        p_valid_i = Signal(reset_less=True)
         pv = Signal(reset_less=True)
-        m.d.comb += p_i_valid.eq(self.p.i_valid_test)
-        m.d.comb += pv.eq(self.p.i_valid & self.p.o_ready)
+        buf_full = Signal(reset_less=True)
+        m.d.comb += p_valid_i.eq(self.p.valid_i_test)
+        m.d.comb += pv.eq(self.p.valid_i & self.p.ready_o)
+        m.d.comb += buf_full.eq(~self.n.ready_i_test & data_valid)
+
+        m.d.comb += self.n.valid_o.eq(data_valid)
+        m.d.comb += self.p._ready_o.eq(~data_valid | self.n.ready_i_test)
+        m.d.sync += data_valid.eq(p_valid_i | buf_full)
 
-        m.d.comb += self.n.o_valid.eq(data_valid)
-        m.d.comb += self.p._o_ready.eq(~data_valid | self.n.i_ready_test)
-        m.d.sync += data_valid.eq(p_i_valid | \
-                                        (~self.n.i_ready_test & data_valid))
         with m.If(pv):
-            m.d.sync += eq(r_data, self.stage.process(self.p.i_data))
-        m.d.comb += eq(self.n.o_data, r_data)
+            m.d.sync += nmoperator.eq(r_data, self.data_r)
+        data_o = self._postprocess(r_data) # XXX TBD, does nothing right now
+        m.d.comb += nmoperator.eq(self.n.data_o, data_o)
 
         return self.m
 
-
 class UnbufferedPipeline2(ControlBase):
     """ A simple pipeline stage with single-clock synchronisation
         and two-way valid/ready synchronised signalling.
@@ -934,80 +592,133 @@ class UnbufferedPipeline2(ControlBase):
 
         Argument: stage.  see Stage API, above
 
-        stage-1   p.i_valid >>in   stage   n.o_valid out>>   stage+1
-        stage-1   p.o_ready <<out  stage   n.i_ready <<in    stage+1
-        stage-1   p.i_data  >>in   stage   n.o_data  out>>   stage+1
+        stage-1   p.valid_i >>in   stage   n.valid_o out>>   stage+1
+        stage-1   p.ready_o <<out  stage   n.ready_i <<in    stage+1
+        stage-1   p.data_i  >>in   stage   n.data_o  out>>   stage+1
                               |             |    |
                               +- process-> buf <-+
         Attributes:
         -----------
-        p.i_data : StageInput, shaped according to ispec
+        p.data_i : StageInput, shaped according to ispec
             The pipeline input
-        p.o_data : StageOutput, shaped according to ospec
+        p.data_o : StageOutput, shaped according to ospec
             The pipeline output
         buf : output_shape according to ospec
             A temporary (buffered) copy of a valid output
             This is HELD if the output is not ready.  It is updated
             SYNCHRONOUSLY.
+
+        Inputs  Temp  Output Data
+        -------   -   -----
+        P P N N ~NiR&  N P   (buf_full)
+        i o i o  NoV   o o
+        V R R V        V R
+
+        -------   -    - -
+        0 0 0 0   0    0 1   process(data_i)
+        0 0 0 1   1    1 0   reg (odata, unchanged)
+        0 0 1 0   0    0 1   process(data_i)
+        0 0 1 1   0    0 1   process(data_i)
+        -------   -    - -
+        0 1 0 0   0    0 1   process(data_i)
+        0 1 0 1   1    1 0   reg (odata, unchanged)
+        0 1 1 0   0    0 1   process(data_i)
+        0 1 1 1   0    0 1   process(data_i)
+        -------   -    - -
+        1 0 0 0   0    1 1   process(data_i)
+        1 0 0 1   1    1 0   reg (odata, unchanged)
+        1 0 1 0   0    1 1   process(data_i)
+        1 0 1 1   0    1 1   process(data_i)
+        -------   -    - -
+        1 1 0 0   0    1 1   process(data_i)
+        1 1 0 1   1    1 0   reg (odata, unchanged)
+        1 1 1 0   0    1 1   process(data_i)
+        1 1 1 1   0    1 1   process(data_i)
+        -------   -    - -
+
+        Note: PoR is *NOT* involved in the above decision-making.
     """
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
 
         buf_full = Signal() # is data valid or not
-        buf = self.stage.ospec() # output type
+        buf = _spec(self.stage.ospec, "r_tmp") # output type
 
         # some temporaries
-        p_i_valid = Signal(reset_less=True)
-        m.d.comb += p_i_valid.eq(self.p.i_valid_test)
+        p_valid_i = Signal(reset_less=True)
+        m.d.comb += p_valid_i.eq(self.p.valid_i_test)
 
-        m.d.comb += self.n.o_valid.eq(buf_full | p_i_valid)
-        m.d.comb += self.p._o_ready.eq(~buf_full)
-        m.d.sync += buf_full.eq(~self.n.i_ready_test & self.n.o_valid)
+        m.d.comb += self.n.valid_o.eq(buf_full | p_valid_i)
+        m.d.comb += self.p._ready_o.eq(~buf_full)
+        m.d.sync += buf_full.eq(~self.n.ready_i_test & self.n.valid_o)
 
-        odata = Mux(buf_full, buf, self.stage.process(self.p.i_data))
-        m.d.comb += eq(self.n.o_data, odata)
-        m.d.sync += eq(buf, self.n.o_data)
+        data_o = Mux(buf_full, buf, self.data_r)
+        data_o = self._postprocess(data_o) # XXX TBD, does nothing right now
+        m.d.comb += nmoperator.eq(self.n.data_o, data_o)
+        m.d.sync += nmoperator.eq(buf, self.n.data_o)
 
         return self.m
 
 
-class PassThroughStage(StageCls):
-    """ a pass-through stage which has its input data spec equal to its output,
-        and "passes through" its data from input to output.
-    """
-    def __init__(self, iospecfn):
-        self.iospecfn = iospecfn
-    def ispec(self): return self.iospecfn()
-    def ospec(self): return self.iospecfn()
-    def process(self, i): return i
-
-
 class PassThroughHandshake(ControlBase):
     """ A control block that delays by one clock cycle.
+
+        Inputs   Temporary          Output Data
+        -------  ------------------  ----- ----
+        P P N N  PiV& PiV| NiR| pvr   N P  (pvr)
+        i o i o  PoR  ~PoR ~NoV       o o
+        V R R V                       V R
+
+        -------   -    -    -   -     - -
+        0 0 0 0   0    1    1   0     1 1   odata (unchanged)
+        0 0 0 1   0    1    0   0     1 0   odata (unchanged)
+        0 0 1 0   0    1    1   0     1 1   odata (unchanged)
+        0 0 1 1   0    1    1   0     1 1   odata (unchanged)
+        -------   -    -    -   -     - -
+        0 1 0 0   0    0    1   0     0 1   odata (unchanged)
+        0 1 0 1   0    0    0   0     0 0   odata (unchanged)
+        0 1 1 0   0    0    1   0     0 1   odata (unchanged)
+        0 1 1 1   0    0    1   0     0 1   odata (unchanged)
+        -------   -    -    -   -     - -
+        1 0 0 0   0    1    1   1     1 1   process(in)
+        1 0 0 1   0    1    0   0     1 0   odata (unchanged)
+        1 0 1 0   0    1    1   1     1 1   process(in)
+        1 0 1 1   0    1    1   1     1 1   process(in)
+        -------   -    -    -   -     - -
+        1 1 0 0   1    1    1   1     1 1   process(in)
+        1 1 0 1   1    1    0   0     1 0   odata (unchanged)
+        1 1 1 0   1    1    1   1     1 1   process(in)
+        1 1 1 1   1    1    1   1     1 1   process(in)
+        -------   -    -    -   -     - -
+
     """
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
+
+        r_data = _spec(self.stage.ospec, "r_tmp") # output type
 
         # temporaries
-        p_i_valid = Signal(reset_less=True)
+        p_valid_i = Signal(reset_less=True)
         pvr = Signal(reset_less=True)
-        m.d.comb += p_i_valid.eq(self.p.i_valid_test)
-        m.d.comb += pvr.eq(p_i_valid & self.p.o_ready)
+        m.d.comb += p_valid_i.eq(self.p.valid_i_test)
+        m.d.comb += pvr.eq(p_valid_i & self.p.ready_o)
 
-        m.d.comb += self.p.o_ready.eq(~self.n.o_valid |  self.n.i_ready_test)
-        m.d.sync += self.n.o_valid.eq(p_i_valid       | ~self.p.o_ready)
+        m.d.comb += self.p.ready_o.eq(~self.n.valid_o |  self.n.ready_i_test)
+        m.d.sync += self.n.valid_o.eq(p_valid_i       | ~self.p.ready_o)
 
-        odata = Mux(pvr, self.stage.process(self.p.i_data), self.n.o_data)
-        m.d.sync += eq(self.n.o_data, odata)
+        odata = Mux(pvr, self.data_r, r_data)
+        m.d.sync += nmoperator.eq(r_data, odata)
+        r_data = self._postprocess(r_data) # XXX TBD, does nothing right now
+        m.d.comb += nmoperator.eq(self.n.data_o, r_data)
 
         return m
 
 
 class RegisterPipeline(UnbufferedPipeline):
     """ A pipeline stage that delays by one clock cycle, creating a
-        sync'd latch out of o_data and o_valid as an indirect byproduct
+        sync'd latch out of data_o and valid_o as an indirect byproduct
         of using PassThroughStage
     """
     def __init__(self, iospecfn):
@@ -1015,74 +726,104 @@ class RegisterPipeline(UnbufferedPipeline):
 
 
 class FIFOControl(ControlBase):
-    """ FIFO Control.  Uses SyncFIFO to store data, coincidentally
+    """ FIFO Control.  Uses Queue to store data, coincidentally
         happens to have same valid/ready signalling as Stage API.
 
-        i_data -> fifo.din -> FIFO -> fifo.dout -> o_data
+        data_i -> fifo.din -> FIFO -> fifo.dout -> data_o
     """
+    def __init__(self, depth, stage, in_multi=None, stage_ctl=False,
+                                     fwft=True, pipe=False):
+        """ FIFO Control
 
-    def __init__(self, depth, stage):
-        """ * iospecfn: specification for incoming and outgoing data
-            * depth   : number of entries in the FIFO
+            * :depth: number of entries in the FIFO
+            * :stage: data processing block
+            * :fwft:  first word fall-thru mode (non-fwft introduces delay)
+            * :pipe:  specifies pipe mode.
 
-            NOTE 1: FPGAs may have trouble with the defaults for SyncFIFO
+            when fwft = True it indicates that transfers may occur
+            combinatorially through stage processing in the same clock cycle.
+            This requires that the Stage be a Moore FSM:
+            https://en.wikipedia.org/wiki/Moore_machine
 
-            NOTE 2: i_data *must* have a shape function.  it can therefore
-                    be a Signal, or a Record, or a RecordObject.
+            when fwft = False it indicates that all output signals are
+            produced only from internal registers or memory, i.e. that the
+            Stage is a Mealy FSM:
+            https://en.wikipedia.org/wiki/Mealy_machine
 
             data is processed (and located) as follows:
 
             self.p  self.stage temp    fn temp  fn  temp  fp   self.n
-            i_data->process()->result->flatten->din.FIFO.dout->flatten(o_data)
+            data_i->process()->result->cat->din.FIFO.dout->cat(data_o)
 
-            yes, really: flatten produces a Cat() which can be assigned to.
-            this is how the FIFO gets de-flattened without needing a de-flatten
+            yes, really: cat produces a Cat() which can be assigned to.
+            this is how the FIFO gets de-catted without needing a de-cat
             function
         """
-
+        self.fwft = fwft
+        self.pipe = pipe
         self.fdepth = depth
-        ControlBase.__init__(self, stage=stage)
+        ControlBase.__init__(self, stage, in_multi, stage_ctl)
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
 
-        # make a FIFO with a signal of equal width to the o_data.
-        (fwidth, _) = self.n.o_data.shape()
-        fifo = SyncFIFO(fwidth, self.fdepth)
+        # make a FIFO with a signal of equal width to the data_o.
+        (fwidth, _) = nmoperator.shape(self.n.data_o)
+        fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe)
         m.submodules.fifo = fifo
 
-        # store result of processing in combinatorial temporary
-        result = self.stage.ospec()
-        m.d.comb += eq(result, self.stage.process(self.p.i_data))
-
-        # connect previous rdy/valid/data - do flatten on i_data
-        # NOTE: cannot do the PrevControl-looking trick because
-        # of need to process the data.  shaaaame....
-        m.d.comb += [fifo.we.eq(self.p.i_valid_test),
-                     self.p.o_ready.eq(fifo.writable),
-                     eq(fifo.din, flatten(result)),
-                   ]
-
-        # next: make the FIFO "look" like a NextControl...
-        fn = NextControl()
-        fn.o_valid, fn.i_ready, fn.o_data = fifo.readable, fifo.re, fifo.dout
-        m.d.comb += fn._connect_out(self.n, fn=flatten) # ...so we can do this!
-
-        # err... that should be all!
+        def processfn(data_i):
+            # store result of processing in combinatorial temporary
+            result = _spec(self.stage.ospec, "r_temp")
+            m.d.comb += nmoperator.eq(result, self.process(data_i))
+            return nmoperator.cat(result)
+
+        ## prev: make the FIFO (Queue object) "look" like a PrevControl...
+        m.submodules.fp = fp = PrevControl()
+        fp.valid_i, fp._ready_o, fp.data_i = fifo.we, fifo.writable, fifo.din
+        m.d.comb += fp._connect_in(self.p, fn=processfn)
+
+        # next: make the FIFO (Queue object) "look" like a NextControl...
+        m.submodules.fn = fn = NextControl()
+        fn.valid_o, fn.ready_i, fn.data_o  = fifo.readable, fifo.re, fifo.dout
+        connections = fn._connect_out(self.n, fn=nmoperator.cat)
+
+        # ok ok so we can't just do the ready/valid eqs straight:
+        # first 2 from connections are the ready/valid, 3rd is data.
+        if self.fwft:
+            m.d.comb += connections[:2] # combinatorial on next ready/valid
+        else:
+            m.d.sync += connections[:2]  # non-fwft mode needs sync
+        data_o = connections[2] # get the data
+        data_o = self._postprocess(data_o) # XXX TBD, does nothing right now
+        m.d.comb += data_o
+
         return m
 
-        # XXX
-        # XXX UNUSED CODE!
-        # XXX
 
-        # prev: make the FIFO "look" like a PrevControl...
-        fp = PrevControl()
-        fp.i_valid, fp._o_ready, fp.i_data = fifo.we, fifo.writable, fifo.din
-        m.d.comb += fp._connect_in(self.p, True, fn=flatten)
+# aka "RegStage".
+class UnbufferedPipeline(FIFOControl):
+    def __init__(self, stage, in_multi=None, stage_ctl=False):
+        FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl,
+                                   fwft=True, pipe=False)
+
+# aka "BreakReadyStage" XXX had to set fwft=True to get it to work
+class PassThroughHandshake(FIFOControl):
+    def __init__(self, stage, in_multi=None, stage_ctl=False):
+        FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl,
+                                   fwft=True, pipe=True)
 
-        # connect next rdy/valid/data - do flatten on o_data
-        m.d.comb += [self.n.o_valid.eq(fifo.readable),
-                     fifo.re.eq(self.n.i_ready_test),
-                     flatten(self.n.o_data).eq(fifo.dout),
-                   ]
+# this is *probably* BufferedHandshake, although test #997 now succeeds.
+class BufferedHandshake(FIFOControl):
+    def __init__(self, stage, in_multi=None, stage_ctl=False):
+        FIFOControl.__init__(self, 2, stage, in_multi, stage_ctl,
+                                   fwft=True, pipe=False)
 
+
+"""
+# this is *probably* SimpleHandshake (note: memory cell size=0)
+class SimpleHandshake(FIFOControl):
+    def __init__(self, stage, in_multi=None, stage_ctl=False):
+        FIFOControl.__init__(self, 0, stage, in_multi, stage_ctl,
+                                   fwft=True, pipe=False)
+"""