update comments
[ieee754fpu.git] / src / add / singlepipe.py
index 32280c6349c917f1e5237eef14b12ef40cbc7ba9..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:
     ----------------
 
     RecordBasedStage:
     ----------------
     connect a chain of pipelines and present the exact same prev/next
     ready/valid/data API.
 
     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:
     ------------------
 
     UnbufferedPipeline:
     ------------------
 
     where data will flow on *every* clock when the conditions are right.
 
     input acceptance conditions are when:
     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:
 
     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
 
     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
 """
 
     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.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 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
+from nmigen.hdl.rec import Record
 
 
-        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)
-
-
-
-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 queue import Queue
+import inspect
 
 
+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.
 
 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)
 
 
     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
     """
     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:
             * 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)
 
         # 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:
 
         # 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.
 
     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
 
                        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
             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,
             this is handled AUTOMATICALLY, here.
 
             Basically this function is the direct equivalent of StageChain,
@@ -607,68 +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.
             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):
         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
 
 
         return eqs
 
-    def _postprocess(self, i):
-        if hasattr(self.stage, "postprocess"):
-            return self.stage.postprocess(i)
-        return i
-
     def set_input(self, i):
     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):
 
     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()
         """ 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
 
         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
 
         # 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
 
 
         return m
 
@@ -680,15 +327,15 @@ class BufferedHandshake(ControlBase):
 
         Argument: stage.  see Stage API above
 
 
         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 ->-+
 
                               |             |
                             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
         intermediate result store [process()].  this is updated combinatorially.
 
         in a non-stall condition, the intermediate result will go into the
@@ -704,54 +351,54 @@ class BufferedHandshake(ControlBase):
     """
 
     def elaborate(self, platform):
     """
 
     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)
 
         # 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)
         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)
         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),
                      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
         ]
 
         # 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
 
         # 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):
 
         # data pass-through conditions
         with self.m.If(npnn):
-            o_data = self._postprocess(result)
-            self.m.d.sync += [self.n.o_valid.eq(p_i_valid), # valid if p_valid
-                              eq(self.n.o_data, o_data),    # 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.
                              ]
         # 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.
-            o_data = self._postprocess(r_data)
-            self.m.d.sync += [self.n.o_valid.eq(1),  # reg empty
-                              eq(self.n.o_data, o_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
                              ]
         # 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
 
 
         return self.m
 
@@ -762,9 +409,9 @@ class SimpleHandshake(ControlBase):
 
         Argument: stage.  see Stage API above
 
 
         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
                               |             |
                               +--process->--^
         Truth Table
@@ -778,61 +425,61 @@ class SimpleHandshake(ControlBase):
         -------   -    -     - -
         0 0 0 0   0    0    >0 0    reg
         0 0 0 1   0    1    >1 0    reg
         -------   -    -     - -
         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(i_data)
-        0 0 1 1   0    0     0 1    process(i_data)
+        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    reg
         0 1 0 1   0    1    >1 0    reg
         -------   -    -     - -
         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(i_data)
-        0 1 1 1   0    0     0 1    process(i_data)
+        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    reg
         1 0 0 1   0    1    >1 0    reg
         -------   -    -     - -
         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(i_data)
-        1 0 1 1   0    0     0 1    process(i_data)
+        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    process(i_data)
-        1 1 0 1   1    1     1 0    process(i_data)
-        1 1 1 0   1    0     1 1    process(i_data)
-        1 1 1 1   1    0     1 1    process(i_data)
+        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):
         -------   -    -     - -
     """
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
 
         r_busy = Signal()
 
         r_busy = Signal()
-        result = self.stage.ospec()
+        result = _spec(self.stage.ospec, "r_tmp")
 
         # establish some combinatorial temporaries
 
         # 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
         ]
 
         # 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
 
         # previous valid and ready
-        with m.If(p_i_valid_p_o_ready):
-            o_data = self._postprocess(result)
+        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
             m.d.sync += [r_busy.eq(1),      # output valid
-                         eq(self.n.o_data, o_data), # update output
+                         nmoperator.eq(self.n.data_o, data_o), # update output
                         ]
         # previous invalid or not ready, however next is accepting
                         ]
         # previous invalid or not ready, however next is accepting
-        with m.Elif(n_i_ready):
-            o_data = self._postprocess(result)
-            m.d.sync += [eq(self.n.o_data, o_data)]
+        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)
             # 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.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
         # 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
 
 
         return self.m
 
@@ -851,9 +498,9 @@ class UnbufferedPipeline(ControlBase):
 
         Argument: stage.  see Stage API, above
 
 
         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
                               |             |
                               |             |
                             r_data        result
                               |             |
@@ -861,9 +508,9 @@ class UnbufferedPipeline(ControlBase):
 
         Attributes:
         -----------
 
         Attributes:
         -----------
-        p.i_data : StageInput, shaped according to ispec
+        p.data_i : StageInput, shaped according to ispec
             The pipeline input
             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.
             The pipeline output
         r_data : input_shape according to ispec
             A temporary (buffered) copy of a prior (valid) input.
@@ -897,41 +544,40 @@ class UnbufferedPipeline(ControlBase):
         1 0 1 0   0    1 1    reg
         1 0 1 1   0    1 1    reg
         -------   -    - -
         1 0 1 0   0    1 1    reg
         1 0 1 1   0    1 1    reg
         -------   -    - -
-        1 1 0 0   0    1 1    process(i_data)
-        1 1 0 1   1    1 0    process(i_data)
-        1 1 1 0   0    1 1    process(i_data)
-        1 1 1 1   0    1 1    process(i_data)
+        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):
         -------   -    - -
 
         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
 
         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
 
         # some temporaries
-        p_i_valid = Signal(reset_less=True)
+        p_valid_i = Signal(reset_less=True)
         pv = Signal(reset_less=True)
         buf_full = Signal(reset_less=True)
         pv = Signal(reset_less=True)
         buf_full = 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)
-        m.d.comb += buf_full.eq(~self.n.i_ready_test & data_valid)
+        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.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 | buf_full)
+        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)
 
         with m.If(pv):
 
         with m.If(pv):
-            m.d.sync += eq(r_data, self.stage.process(self.p.i_data))
-        o_data = self._postprocess(r_data)
-        m.d.comb += eq(self.n.o_data, o_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
 
 
         return self.m
 
-
 class UnbufferedPipeline2(ControlBase):
     """ A simple pipeline stage with single-clock synchronisation
         and two-way valid/ready synchronised signalling.
 class UnbufferedPipeline2(ControlBase):
     """ A simple pipeline stage with single-clock synchronisation
         and two-way valid/ready synchronised signalling.
@@ -946,16 +592,16 @@ class UnbufferedPipeline2(ControlBase):
 
         Argument: stage.  see Stage API, above
 
 
         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:
         -----------
                               |             |    |
                               +- process-> buf <-+
         Attributes:
         -----------
-        p.i_data : StageInput, shaped according to ispec
+        p.data_i : StageInput, shaped according to ispec
             The pipeline input
             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
             The pipeline output
         buf : output_shape according to ospec
             A temporary (buffered) copy of a valid output
@@ -969,64 +615,52 @@ class UnbufferedPipeline2(ControlBase):
         V R R V        V R
 
         -------   -    - -
         V R R V        V R
 
         -------   -    - -
-        0 0 0 0   0    0 1   process(i_data)
+        0 0 0 0   0    0 1   process(data_i)
         0 0 0 1   1    1 0   reg (odata, unchanged)
         0 0 0 1   1    1 0   reg (odata, unchanged)
-        0 0 1 0   0    0 1   process(i_data)
-        0 0 1 1   0    0 1   process(i_data)
+        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(i_data)
+        0 1 0 0   0    0 1   process(data_i)
         0 1 0 1   1    1 0   reg (odata, unchanged)
         0 1 0 1   1    1 0   reg (odata, unchanged)
-        0 1 1 0   0    0 1   process(i_data)
-        0 1 1 1   0    0 1   process(i_data)
+        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(i_data)
+        1 0 0 0   0    1 1   process(data_i)
         1 0 0 1   1    1 0   reg (odata, unchanged)
         1 0 0 1   1    1 0   reg (odata, unchanged)
-        1 0 1 0   0    1 1   process(i_data)
-        1 0 1 1   0    1 1   process(i_data)
+        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(i_data)
+        1 1 0 0   0    1 1   process(data_i)
         1 1 0 1   1    1 0   reg (odata, unchanged)
         1 1 0 1   1    1 0   reg (odata, unchanged)
-        1 1 1 0   0    1 1   process(i_data)
-        1 1 1 1   0    1 1   process(i_data)
+        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):
         -------   -    - -
 
         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_full = Signal() # is data valid or not
-        buf = self.stage.ospec() # output type
+        buf = _spec(self.stage.ospec, "r_tmp") # output type
 
         # some temporaries
 
         # 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)
 
 
-        o_data = Mux(buf_full, buf, self.stage.process(self.p.i_data))
-        if hasattr(self.stage, "postprocess"):
-            o_data = self.stage.postprocess(o_data)
-        m.d.comb += eq(self.n.o_data, o_data)
-        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
 
 
 
         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.
 
 class PassThroughHandshake(ControlBase):
     """ A control block that delays by one clock cycle.
 
@@ -1061,31 +695,30 @@ class PassThroughHandshake(ControlBase):
     """
 
     def elaborate(self, platform):
     """
 
     def elaborate(self, platform):
-        self.m = m = ControlBase._elaborate(self, platform)
+        self.m = m = ControlBase.elaborate(self, platform)
 
 
-        r_data = self.stage.ospec() # output type
+        r_data = _spec(self.stage.ospec, "r_tmp") # output type
 
         # temporaries
 
         # temporaries
-        p_i_valid = Signal(reset_less=True)
+        p_valid_i = Signal(reset_less=True)
         pvr = 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), r_data)
-        m.d.sync += eq(r_data, odata)
-        if hasattr(self.stage, "postprocess"):
-            r_data = self.stage.postprocess(r_data)
-        m.d.comb += eq(self.n.o_data, r_data)
+        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
 
         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):
         of using PassThroughStage
     """
     def __init__(self, iospecfn):
@@ -1093,78 +726,104 @@ class RegisterPipeline(UnbufferedPipeline):
 
 
 class FIFOControl(ControlBase):
 
 
 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.
 
         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, fwft=True, buffered=False):
+    def __init__(self, depth, stage, in_multi=None, stage_ctl=False,
+                                     fwft=True, pipe=False):
         """ FIFO Control
 
         """ FIFO Control
 
-            * depth: number of entries in the FIFO
-            * stage: data processing block
-            * fwft : first word fall-thru mode (non-fwft introduces delay)
-            * buffered: use buffered FIFO (introduces extra cycle delay)
+            * :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
-                    (fwft=True, buffered=False)
+            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
 
             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
         """
             function
         """
-
-        assert not (fwft and buffered), "buffered cannot do fwft"
-        if buffered:
-            depth += 1
         self.fwft = fwft
         self.fwft = fwft
-        self.buffered = buffered
+        self.pipe = pipe
         self.fdepth = depth
         self.fdepth = depth
-        ControlBase.__init__(self, stage=stage)
+        ControlBase.__init__(self, stage, in_multi, stage_ctl)
 
     def elaborate(self, platform):
 
     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()
-        if self.buffered:
-            fifo = SyncFIFOBuffered(fwidth, self.fdepth)
-        else:
-            fifo = SyncFIFO(fwidth, self.fdepth, fwft=self.fwft)
+        # 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
 
         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)),
-                   ]
-
-        # connect next rdy/valid/data - do flatten on o_data
-        connections = [self.n.o_valid.eq(fifo.readable),
-                     fifo.re.eq(self.n.i_ready_test),
-                   ]
-        if self.fwft or self.buffered:
-            m.d.comb += connections
+        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:
         else:
-            m.d.sync += connections # unbuffered fwft mode needs sync
-        o_data = flatten(self.n.o_data).eq(fifo.dout)
-        if hasattr(self.stage, "postprocess"):
-            o_data = self.stage.postprocess(o_data)
-        m.d.comb += o_data
+            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
 
         return m
+
+
+# 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)
+
+# 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)
+"""