From 489eedc41010016a8e1a01c3baf75d333fc6cd1c Mon Sep 17 00:00:00 2001 From: Luke Kenneth Casson Leighton Date: Mon, 29 Apr 2019 04:49:11 +0100 Subject: [PATCH] split stageapi into separate module, move ControlBase to singlepipe --- src/add/iocontrol.py | 321 ------------------------------------------ src/add/singlepipe.py | 170 +++++++++++++++++++++- src/add/stageapi.py | 258 +++++++++++++++++++++++++++++++++ 3 files changed, 424 insertions(+), 325 deletions(-) create mode 100644 src/add/stageapi.py diff --git a/src/add/iocontrol.py b/src/add/iocontrol.py index 81142c29..de1c938e 100644 --- a/src/add/iocontrol.py +++ b/src/add/iocontrol.py @@ -164,15 +164,6 @@ class RecordObject(Record): return list(self) -def _spec(fn, name=None): - if name is None: - return fn() - varnames = dict(inspect.getmembers(fn.__code__))['co_varnames'] - if 'name' in varnames: - return fn(name=name) - return fn() - - class PrevControl(Elaboratable): """ contains signals that come *from* the previous stage (both in and out) * valid_i: previous stage indicating all incoming data is valid. @@ -312,315 +303,3 @@ class NextControl(Elaboratable): def ports(self): return list(self) - -class StageCls(metaclass=ABCMeta): - """ Class-based "Stage" API. requires instantiation (after derivation) - - see "Stage API" above.. Note: python does *not* require derivation - from this class. All that is required is that the pipelines *have* - the functions listed in this class. Derivation from this class - is therefore merely a "courtesy" to maintainers. - """ - @abstractmethod - def ispec(self): pass # REQUIRED - @abstractmethod - def ospec(self): pass # REQUIRED - #@abstractmethod - #def setup(self, m, i): pass # OPTIONAL - #@abstractmethod - #def process(self, i): pass # OPTIONAL - - -class Stage(metaclass=ABCMeta): - """ Static "Stage" API. does not require instantiation (after derivation) - - see "Stage API" above. Note: python does *not* require derivation - from this class. All that is required is that the pipelines *have* - the functions listed in this class. Derivation from this class - is therefore merely a "courtesy" to maintainers. - """ - @staticmethod - @abstractmethod - def ispec(): pass - - @staticmethod - @abstractmethod - def ospec(): pass - - #@staticmethod - #@abstractmethod - #def setup(m, i): pass - - #@staticmethod - #@abstractmethod - #def process(i): pass - - -class StageChain(StageCls): - """ pass in a list of stages, and they will automatically be - chained together via their input and output specs into a - combinatorial chain, to create one giant combinatorial block. - - the end result basically conforms to the exact same Stage API. - - * input to this class will be the input of the first stage - * output of first stage goes into input of second - * output of second goes into input into third - * ... (etc. etc.) - * the output of this class will be the output of the last stage - - NOTE: whilst this is very similar to ControlBase.connect(), it is - *really* important to appreciate that StageChain is pure - combinatorial and bypasses (does not involve, at all, ready/valid - signalling of any kind). - - ControlBase.connect on the other hand respects, connects, and uses - ready/valid signalling. - - Arguments: - - * :chain: a chain of combinatorial blocks conforming to the Stage API - NOTE: StageChain.ispec and ospect have to have something - to return (beginning and end specs of the chain), - therefore the chain argument must be non-zero length - - * :specallocate: if set, new input and output data will be allocated - and connected (eq'd) to each chained Stage. - in some cases if this is not done, the nmigen warning - "driving from two sources, module is being flattened" - will be issued. - - NOTE: do NOT use StageChain with combinatorial blocks that have - side-effects (state-based / clock-based input) or conditional - (inter-chain) dependencies, unless you really know what you are doing. - """ - def __init__(self, chain, specallocate=False): - assert len(chain) > 0, "stage chain must be non-zero length" - self.chain = chain - self.specallocate = specallocate - - def ispec(self): - """ returns the ispec of the first of the chain - """ - return _spec(self.chain[0].ispec, "chainin") - - def ospec(self): - """ returns the ospec of the last of the chain - """ - return _spec(self.chain[-1].ospec, "chainout") - - def _specallocate_setup(self, m, i): - for (idx, c) in enumerate(self.chain): - if hasattr(c, "setup"): - c.setup(m, i) # stage may have some module stuff - ofn = self.chain[idx].ospec # last assignment survives - o = _spec(ofn, 'chainin%d' % idx) - m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o" - if idx == len(self.chain)-1: - break - ifn = self.chain[idx+1].ispec # new input on next loop - i = _spec(ifn, 'chainin%d' % (idx+1)) - m.d.comb += nmoperator.eq(i, o) # assign to next input - return o # last loop is the output - - def _noallocate_setup(self, m, i): - for (idx, c) in enumerate(self.chain): - if hasattr(c, "setup"): - c.setup(m, i) # stage may have some module stuff - i = o = c.process(i) # store input into "o" - return o # last loop is the output - - def setup(self, m, i): - if self.specallocate: - self.o = self._specallocate_setup(m, i) - else: - self.o = self._noallocate_setup(m, i) - - def process(self, i): - return self.o # conform to Stage API: return last-loop output - - -class StageHelper(Stage): - """ a convenience wrapper around something that is Stage-API-compliant. - (that "something" may be a static class, for example). - - StageHelper happens to also be compliant with the Stage API, - except that all the "optional" functions are provided - (hence the designation "convenience wrapper") - """ - def __init__(self, stage): - self.stage = stage - - def ospec(self, name): - assert self.stage is not None - return _spec(self.stage.ospec, name) - - def ispec(self, name): - assert self.stage is not None - return _spec(self.stage.ispec, name) - - def process(self, i): - if self.stage and hasattr(self.stage, "process"): - return self.stage.process(i) - return i - - def setup(self, m, i): - if self.stage is not None and hasattr(self.stage, "setup"): - self.stage.setup(m, i) - - def _postprocess(self, i): # XXX DISABLED - return i # RETURNS INPUT - if hasattr(self.stage, "postprocess"): - return self.stage.postprocess(i) - return i - - -class ControlBase(StageHelper, Elaboratable): - """ Common functions for Pipeline API. Note: a "pipeline stage" only - exists (conceptually) when a ControlBase derivative is handed - a Stage (combinatorial block) - - NOTE: ControlBase derives from StageHelper, making it accidentally - compliant with the Stage API. Using those functions directly - *BYPASSES* a ControlBase instance ready/valid signalling, which - clearly should not be done without a really, really good reason. - """ - def __init__(self, stage=None, in_multi=None, stage_ctl=False): - """ Base class containing ready/valid/data to previous and next stages - - * p: contains ready/valid to the previous stage - * n: contains ready/valid to the next stage - - Except when calling Controlbase.connect(), user must also: - * add data_i member to PrevControl (p) and - * add data_o member to NextControl (n) - Calling ControlBase._new_data is a good way to do that. - """ - StageHelper.__init__(self, stage) - - # set up input and output IO ACK (prev/next ready/valid) - self.p = PrevControl(in_multi, stage_ctl) - self.n = NextControl(stage_ctl) - - # set up the input and output data - if stage is not None: - self._new_data(self, self, "data") - - def _new_data(self, p, n, name): - """ allocates new data_i and data_o - """ - self.p.data_i = _spec(p.stage.ispec, "%s_i" % name) - self.n.data_o = _spec(n.stage.ospec, "%s_o" % name) - - @property - def data_r(self): - return self.process(self.p.data_i) - - def connect_to_next(self, nxt): - """ helper function to connect to the next stage data/valid/ready. - """ - return self.n.connect_to_next(nxt.p) - - def _connect_in(self, prev): - """ internal helper function to connect stage to an input source. - do not use to connect stage-to-stage! - """ - return self.p._connect_in(prev.p) - - def _connect_out(self, nxt): - """ internal helper function to connect stage to an output source. - do not use to connect stage-to-stage! - """ - return self.n._connect_out(nxt.n) - - def connect(self, pipechain): - """ connects a chain (list) of Pipeline instances together and - links them to this ControlBase instance: - - in <----> self <---> out - | ^ - v | - [pipe1, pipe2, pipe3, pipe4] - | ^ | ^ | ^ - v | v | v | - out---in out--in out---in - - Also takes care of allocating data_i/data_o, by looking up - the data spec for each end of the pipechain. i.e It is NOT - necessary to allocate self.p.data_i or self.n.data_o manually: - this is handled AUTOMATICALLY, here. - - Basically this function is the direct equivalent of StageChain, - except that unlike StageChain, the Pipeline logic is followed. - - Just as StageChain presents an object that conforms to the - Stage API from a list of objects that also conform to the - Stage API, an object that calls this Pipeline connect function - has the exact same pipeline API as the list of pipline objects - it is called with. - - Thus it becomes possible to build up larger chains recursively. - More complex chains (multi-input, multi-output) will have to be - done manually. - - Argument: - - * :pipechain: - a sequence of ControlBase-derived classes - (must be one or more in length) - - Returns: - - * a list of eq assignments that will need to be added in - an elaborate() to m.d.comb - """ - assert len(pipechain) > 0, "pipechain must be non-zero length" - eqs = [] # collated list of assignment statements - - # connect inter-chain - for i in range(len(pipechain)-1): - pipe1 = pipechain[i] # earlier - pipe2 = pipechain[i+1] # later (by 1) - eqs += pipe1.connect_to_next(pipe2) # earlier n to later p - - # connect front and back of chain to ourselves - front = pipechain[0] # first in chain - end = pipechain[-1] # last in chain - self._new_data(front, end, "chain") # NOTE: REPLACES existing data - eqs += front._connect_in(self) # front p to our p - eqs += end._connect_out(self) # end n to out n - - return eqs - - def set_input(self, i): - """ helper function to set the input data (used in unit tests) - """ - return nmoperator.eq(self.p.data_i, i) - - def __iter__(self): - yield from self.p # yields ready/valid/data (data also gets yielded) - yield from self.n # ditto - - def ports(self): - return list(self) - - def elaborate(self, platform): - """ handles case where stage has dynamic ready/valid functions - """ - m = Module() - m.submodules.p = self.p - m.submodules.n = self.n - - self.setup(m, self.p.data_i) - - if not self.p.stage_ctl: - return m - - # intercept the previous (outgoing) "ready", combine with stage ready - m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready) - - # intercept the next (incoming) "ready" and combine it with data valid - sdv = self.stage.d_valid(self.n.ready_i) - m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv) - - return m - diff --git a/src/add/singlepipe.py b/src/add/singlepipe.py index 5808d8a9..ebbb1e9b 100644 --- a/src/add/singlepipe.py +++ b/src/add/singlepipe.py @@ -27,6 +27,18 @@ StageChain, however when passed to UnbufferedPipeline they can be used to introduce a single clock delay. + ControlBase: + ----------- + + The base class for pipelines. Contains previous and next ready/valid/data. + Also has an extremely useful "connect" function that can be used to + connect a chain of pipelines and present the exact same prev/next + ready/valid/data API. + + Note: pipelines basically do not become pipelines as such until + handed to a derivative of ControlBase. ControlBase itself is *not* + strictly considered a pipeline class. Wishbone and AXI4 (master or + slave) could be derived from ControlBase, for example. UnbufferedPipeline: ------------------ @@ -121,7 +133,7 @@ from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable from nmigen.cli import verilog, rtlil from nmigen.lib.fifo import SyncFIFO, SyncFIFOBuffered from nmigen.hdl.ast import ArrayProxy -from nmigen.hdl.rec import Record, Layout +from nmigen.hdl.rec import Record from abc import ABCMeta, abstractmethod from collections.abc import Sequence, Iterable @@ -130,11 +142,161 @@ from queue import Queue import inspect import nmoperator -from iocontrol import (Object, RecordObject, _spec, - PrevControl, NextControl, StageCls, Stage, - ControlBase, StageChain) +from iocontrol import (Object, RecordObject) +from stageapi import (_spec, PrevControl, NextControl, StageCls, Stage, + StageChain, StageHelper) + +class ControlBase(StageHelper, Elaboratable): + """ Common functions for Pipeline API. Note: a "pipeline stage" only + exists (conceptually) when a ControlBase derivative is handed + a Stage (combinatorial block) + + NOTE: ControlBase derives from StageHelper, making it accidentally + compliant with the Stage API. Using those functions directly + *BYPASSES* a ControlBase instance ready/valid signalling, which + clearly should not be done without a really, really good reason. + """ + def __init__(self, stage=None, in_multi=None, stage_ctl=False): + """ Base class containing ready/valid/data to previous and next stages + + * p: contains ready/valid to the previous stage + * n: contains ready/valid to the next stage + + Except when calling Controlbase.connect(), user must also: + * add data_i member to PrevControl (p) and + * add data_o member to NextControl (n) + Calling ControlBase._new_data is a good way to do that. + """ + StageHelper.__init__(self, stage) + + # set up input and output IO ACK (prev/next ready/valid) + self.p = PrevControl(in_multi, stage_ctl) + self.n = NextControl(stage_ctl) + + # set up the input and output data + if stage is not None: + self._new_data(self, self, "data") + + def _new_data(self, p, n, name): + """ allocates new data_i and data_o + """ + self.p.data_i = _spec(p.stage.ispec, "%s_i" % name) + self.n.data_o = _spec(n.stage.ospec, "%s_o" % name) + + @property + def data_r(self): + return self.process(self.p.data_i) + + def connect_to_next(self, nxt): + """ helper function to connect to the next stage data/valid/ready. + """ + return self.n.connect_to_next(nxt.p) + + def _connect_in(self, prev): + """ internal helper function to connect stage to an input source. + do not use to connect stage-to-stage! + """ + return self.p._connect_in(prev.p) + + def _connect_out(self, nxt): + """ internal helper function to connect stage to an output source. + do not use to connect stage-to-stage! + """ + return self.n._connect_out(nxt.n) + + def connect(self, pipechain): + """ connects a chain (list) of Pipeline instances together and + links them to this ControlBase instance: + + in <----> self <---> out + | ^ + v | + [pipe1, pipe2, pipe3, pipe4] + | ^ | ^ | ^ + v | v | v | + out---in out--in out---in + + Also takes care of allocating data_i/data_o, by looking up + the data spec for each end of the pipechain. i.e It is NOT + necessary to allocate self.p.data_i or self.n.data_o manually: + this is handled AUTOMATICALLY, here. + + Basically this function is the direct equivalent of StageChain, + except that unlike StageChain, the Pipeline logic is followed. + + Just as StageChain presents an object that conforms to the + Stage API from a list of objects that also conform to the + Stage API, an object that calls this Pipeline connect function + has the exact same pipeline API as the list of pipline objects + it is called with. + + Thus it becomes possible to build up larger chains recursively. + More complex chains (multi-input, multi-output) will have to be + done manually. + + Argument: + + * :pipechain: - a sequence of ControlBase-derived classes + (must be one or more in length) + + Returns: + + * a list of eq assignments that will need to be added in + an elaborate() to m.d.comb + """ + assert len(pipechain) > 0, "pipechain must be non-zero length" + eqs = [] # collated list of assignment statements + + # connect inter-chain + for i in range(len(pipechain)-1): + pipe1 = pipechain[i] # earlier + pipe2 = pipechain[i+1] # later (by 1) + eqs += pipe1.connect_to_next(pipe2) # earlier n to later p + + # connect front and back of chain to ourselves + front = pipechain[0] # first in chain + end = pipechain[-1] # last in chain + self._new_data(front, end, "chain") # NOTE: REPLACES existing data + eqs += front._connect_in(self) # front p to our p + eqs += end._connect_out(self) # end n to out n + + return eqs + + def set_input(self, i): + """ helper function to set the input data (used in unit tests) + """ + return nmoperator.eq(self.p.data_i, i) + + def __iter__(self): + yield from self.p # yields ready/valid/data (data also gets yielded) + yield from self.n # ditto + + def ports(self): + return list(self) + + def elaborate(self, platform): + """ handles case where stage has dynamic ready/valid functions + """ + m = Module() + m.submodules.p = self.p + m.submodules.n = self.n + + self.setup(m, self.p.data_i) + + if not self.p.stage_ctl: + return m + + # intercept the previous (outgoing) "ready", combine with stage ready + m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready) + + # intercept the next (incoming) "ready" and combine it with data valid + sdv = self.stage.d_valid(self.n.ready_i) + m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv) + + return m + class RecordBasedStage(Stage): """ convenience class which provides a Records-based layout. honestly it's a lot easier just to create a direct Records-based diff --git a/src/add/stageapi.py b/src/add/stageapi.py new file mode 100644 index 00000000..9217c1fe --- /dev/null +++ b/src/add/stageapi.py @@ -0,0 +1,258 @@ +""" Stage API + + Associated development bugs: + * http://bugs.libre-riscv.org/show_bug.cgi?id=64 + * http://bugs.libre-riscv.org/show_bug.cgi?id=57 + + Stage API: + --------- + + stage requires compliance with a strict API that may be + implemented in several means, including as a static class. + + Stages do not HOLD data, and they definitely do not contain + signalling (ready/valid). They do however specify the FORMAT + of the incoming and outgoing data, and they provide a means to + PROCESS that data (from incoming format to outgoing format). + + Stage Blocks really must be combinatorial blocks. It would be ok + to have input come in from sync'd sources (clock-driven) however by + doing so they would no longer be deterministic, and chaining such + blocks with such side-effects together could result in unexpected, + unpredictable, unreproduceable behaviour. + So generally to be avoided, then unless you know what you are doing. + + the methods of a stage instance must be as follows: + + * ispec() - Input data format specification. Takes a bit of explaining. + The requirements are: something that eventually derives from + nmigen Value must be returned *OR* an iterator or iterable + or sequence (list, tuple etc.) or generator must *yield* + thing(s) that (eventually) derive from the nmigen Value class. + + Complex to state, very simple in practice: + see test_buf_pipe.py for over 25 worked examples. + + * ospec() - Output data format specification. + format requirements identical to ispec. + + * process(m, i) - Optional function for processing ispec-formatted data. + returns a combinatorial block of a result that + may be assigned to the output, by way of the "nmoperator.eq" + function. Note that what is returned here can be + extremely flexible. Even a dictionary can be returned + as long as it has fields that match precisely with the + Record into which its values is intended to be assigned. + Again: see example unit tests for details. + + * setup(m, i) - Optional function for setting up submodules. + may be used for more complex stages, to link + the input (i) to submodules. must take responsibility + for adding those submodules to the module (m). + the submodules must be combinatorial blocks and + must have their inputs and output linked combinatorially. + + Both StageCls (for use with non-static classes) and Stage (for use + by static classes) are abstract classes from which, for convenience + and as a courtesy to other developers, anything conforming to the + Stage API may *choose* to derive. See Liskov Substitution Principle: + https://en.wikipedia.org/wiki/Liskov_substitution_principle + + StageChain: + ---------- + + A useful combinatorial wrapper around stages that chains them together + and then presents a Stage-API-conformant interface. By presenting + the same API as the stages it wraps, it can clearly be used recursively. + + StageHelper: + ---------- + + A convenience wrapper around a Stage-API-compliant "thing" which + complies with the Stage API and provides mandatory versions of + all the optional bits. +""" + +from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable +from nmigen.cli import verilog, rtlil +from nmigen.hdl.rec import Record + +from abc import ABCMeta, abstractmethod +from collections.abc import Sequence, Iterable +from collections import OrderedDict +import inspect + +from iocontrol import PrevControl, NextControl +import nmoperator + + +def _spec(fn, name=None): + if name is None: + return fn() + varnames = dict(inspect.getmembers(fn.__code__))['co_varnames'] + if 'name' in varnames: + return fn(name=name) + return fn() + + +class StageCls(metaclass=ABCMeta): + """ Class-based "Stage" API. requires instantiation (after derivation) + + see "Stage API" above.. Note: python does *not* require derivation + from this class. All that is required is that the pipelines *have* + the functions listed in this class. Derivation from this class + is therefore merely a "courtesy" to maintainers. + """ + @abstractmethod + def ispec(self): pass # REQUIRED + @abstractmethod + def ospec(self): pass # REQUIRED + #@abstractmethod + #def setup(self, m, i): pass # OPTIONAL + #@abstractmethod + #def process(self, i): pass # OPTIONAL + + +class Stage(metaclass=ABCMeta): + """ Static "Stage" API. does not require instantiation (after derivation) + + see "Stage API" above. Note: python does *not* require derivation + from this class. All that is required is that the pipelines *have* + the functions listed in this class. Derivation from this class + is therefore merely a "courtesy" to maintainers. + """ + @staticmethod + @abstractmethod + def ispec(): pass + + @staticmethod + @abstractmethod + def ospec(): pass + + #@staticmethod + #@abstractmethod + #def setup(m, i): pass + + #@staticmethod + #@abstractmethod + #def process(i): pass + + +class StageChain(StageCls): + """ pass in a list of stages, and they will automatically be + chained together via their input and output specs into a + combinatorial chain, to create one giant combinatorial block. + + the end result basically conforms to the exact same Stage API. + + * input to this class will be the input of the first stage + * output of first stage goes into input of second + * output of second goes into input into third + * ... (etc. etc.) + * the output of this class will be the output of the last stage + + NOTE: whilst this is very similar to ControlBase.connect(), it is + *really* important to appreciate that StageChain is pure + combinatorial and bypasses (does not involve, at all, ready/valid + signalling of any kind). + + ControlBase.connect on the other hand respects, connects, and uses + ready/valid signalling. + + Arguments: + + * :chain: a chain of combinatorial blocks conforming to the Stage API + NOTE: StageChain.ispec and ospect have to have something + to return (beginning and end specs of the chain), + therefore the chain argument must be non-zero length + + * :specallocate: if set, new input and output data will be allocated + and connected (eq'd) to each chained Stage. + in some cases if this is not done, the nmigen warning + "driving from two sources, module is being flattened" + will be issued. + + NOTE: do NOT use StageChain with combinatorial blocks that have + side-effects (state-based / clock-based input) or conditional + (inter-chain) dependencies, unless you really know what you are doing. + """ + def __init__(self, chain, specallocate=False): + assert len(chain) > 0, "stage chain must be non-zero length" + self.chain = chain + self.specallocate = specallocate + + def ispec(self): + """ returns the ispec of the first of the chain + """ + return _spec(self.chain[0].ispec, "chainin") + + def ospec(self): + """ returns the ospec of the last of the chain + """ + return _spec(self.chain[-1].ospec, "chainout") + + def _specallocate_setup(self, m, i): + for (idx, c) in enumerate(self.chain): + if hasattr(c, "setup"): + c.setup(m, i) # stage may have some module stuff + ofn = self.chain[idx].ospec # last assignment survives + o = _spec(ofn, 'chainin%d' % idx) + m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o" + if idx == len(self.chain)-1: + break + ifn = self.chain[idx+1].ispec # new input on next loop + i = _spec(ifn, 'chainin%d' % (idx+1)) + m.d.comb += nmoperator.eq(i, o) # assign to next input + return o # last loop is the output + + def _noallocate_setup(self, m, i): + for (idx, c) in enumerate(self.chain): + if hasattr(c, "setup"): + c.setup(m, i) # stage may have some module stuff + i = o = c.process(i) # store input into "o" + return o # last loop is the output + + def setup(self, m, i): + if self.specallocate: + self.o = self._specallocate_setup(m, i) + else: + self.o = self._noallocate_setup(m, i) + + def process(self, i): + return self.o # conform to Stage API: return last-loop output + + +class StageHelper(Stage): + """ a convenience wrapper around something that is Stage-API-compliant. + (that "something" may be a static class, for example). + + StageHelper happens to also be compliant with the Stage API, + except that all the "optional" functions are provided + (hence the designation "convenience wrapper") + """ + def __init__(self, stage): + self.stage = stage + + def ospec(self, name): + assert self.stage is not None + return _spec(self.stage.ospec, name) + + def ispec(self, name): + assert self.stage is not None + return _spec(self.stage.ispec, name) + + def process(self, i): + if self.stage and hasattr(self.stage, "process"): + return self.stage.process(i) + return i + + def setup(self, m, i): + if self.stage is not None and hasattr(self.stage, "setup"): + self.stage.setup(m, i) + + def _postprocess(self, i): # XXX DISABLED + return i # RETURNS INPUT + if hasattr(self.stage, "postprocess"): + return self.stage.postprocess(i) + return i + -- 2.30.2