#!/usr/bin/python

# Inspired by Mark S. Miller's whenUpdated and reactToUpdate protocol
# described in
# http://www.erights.org/javadoc/org/erights/e/elib/slot/EverReporter.html
# and
# http://www.erights.org/javadoc/org/erights/e/elib/slot/EverReactor.html
# with the following differences: no generation numbers; three methods
# instead of two; and a differing number of messages, in either the
# fast-changing extreme or the slow-changing extreme, three messages
# per communicated change instead of two.

# This protocol shares with Miller's protocol and with MESI the virtue
# that either many writes and no reads, or many reads and no writes,
# result in no communication; it shares with Miller's protocol the
# virtue that it degrades to polling in the case where updates happen
# very frequently (it won't send change notifications more often than
# the sink can receive them) and to asynchronous notification in the
# case where updates happen very rarely.

# A ValueSource accepts just one message: get_value(self, sink), where
# sink is a ValueSink.  When the ValueSource has a value --- possibly
# immediately --- it calls sink.send_value(newval).  At some later
# time, it calls sink.retract_value(), and then discards the sink.

# A Formula implements the ValueSource interface, but it also has a
# list of ValueSources it draws inputs from.  The Formula has a state,
# either unknown, working, or known.  The Formula can only be
# "unknown" if no sinks currently care about its answer; the first
# time someone calls get_value on it, it changes to "working".

# A "working" Formula tries to get values from each of its
# ValueSources; on transition into this state, it passes them each a
# FormulaInputSink that will deposit the value in the relevant place
# inside it the Formula.  If any of the ValueSources calls
# retract_value() during this time, it must immediately be polled
# again with the same FormulaInputSink.

# Eventually, we hope, all the inputs of the Formula are known.  At
# this point, the Formula changes from "working" to "known" state, and
# at this point calls send_value() on any sinks that cared about its
# output.  In "known" state, if any input calls retract_value(), the
# Formula becomes "unknown" and calls retract_value() on all the sinks
# attached to its output.

# To accommodate ref-counting nicely, the Formula extends ValueSource
# (rather than having a ValueSource), since "working" must flow from
# the ValueSource interface to the FormulaInputSinks, while "known"
# flows the other way; while the Formula has a list of its input
# ValueSources, the FormulaInputSinks are ephemeral objects with
# references to the Formula.  This will impede the garbage-collection
# of "working" formulas.  Perhaps the ValueSources should reference
# their sinks weakly.

from __future__ import nested_scopes
import operator

def immediate_evaluator(thunk): thunk()

class QueuedEvaluator(object):
    def __init__(self): self.queue = []
    def __call__(self, thunk): self.queue.append(thunk)
    def run(self):
        while self.queue: self.queue.pop(0)()

class ValueSource(object):
    def __init__(self, evaluator=immediate_evaluator):
        self.value = None
        self.sinks = []
        self.evaluator = [evaluator]
    def delay(self, thunk):
        self.evaluator[0](thunk)
    def get_value(self, sink):
        self.sinks.append(sink)
        if self.value:
            value = self.value[0]
            self.delay(lambda: sink.send_value(value))
    def send_value(self, new_value):
        assert not self.value
        self.value = [new_value]
        for sink in self.sinks:
            def value_sender(sink_contour):
                return lambda: sink_contour.send_value(new_value)
            self.delay(value_sender(sink))
    def retract_value(self):
        assert self.value
        self.value = None
        sinks = self.sinks
        self.sinks = []
        for sink in sinks:
            def value_retractor(sink_contour):
                return lambda: sink_contour.retract_value()
            self.delay(value_retractor(sink))

def all(some_boolean_list):
    for x in some_boolean_list:
        if not x:
            return False
    return True

class unknown(object): pass
class working(object): pass
class known(object): pass

class FormulaInputSink(object):
    def __init__(self, formula, arg_index):
        (self.formula, self.arg_index) = (formula, arg_index)
    def send_value(self, new_value):
        self.formula.send_input(self.arg_index, new_value)
    def retract_value(self):
        self.formula.retract_input(self.arg_index)

class Formula(ValueSource):
    def __init__(self, formula, args, evaluator=immediate_evaluator):
        ValueSource.__init__(self, evaluator=evaluator)
        (self.formula, self.args) = ([formula], args)
        self.state = unknown
        self.input_values = [None for arg in self.args]
    def get_value(self, sink):
        if self.state is unknown:
            self.start_working()
        return ValueSource.get_value(self, sink)
    def start_working(self):
        assert self.state is unknown
        for ii in range(len(self.input_values)):
            if not self.input_values[ii]:
                self.request_input(ii)
        self.state = working
        self.try_to_finish_working()
    def finish_working(self):
        assert self.state is working
        value = apply(self.formula[0], [x for (x,) in self.input_values])
        self.send_value(value)
        self.state = known
    def send_input(self, index, new_value):
        assert not self.input_values[index]
        assert not self.value   # XXX is this wrong?
        self.input_values[index] = [new_value]
        self.try_to_finish_working()
    def try_to_finish_working(self):
        if self.state is working and all(self.input_values):
            self.finish_working()
    def retract_input(self, index):
        if self.value:
            self.retract_value()
        self.input_values[index] = None
        if self.state is working:
            self.request_input(index)
        else:
            self.state = unknown
    def request_input(self, index):
        assert not self.input_values[index]
        self.args[index].get_value(FormulaInputSink(self, index))

def ok(a, b): assert a == b, (a, b)
def test():
    three = Formula(lambda: 3, [])
    ok(three.value, None)
    out = ValueSource()
    three.get_value(out)
    ok(three.value, [3])
    ok(out.value, [3])

    four = Formula(lambda: 4, [])
    ok(four.value, None)

    seven = Formula(operator.add, [three, four])
    ok(four.value, None)
    ok(seven.value, None)

    seven.get_value(ValueSource())
    ok(seven.value, [7])

    in1 = ValueSource()
    in2 = ValueSource()
    adder = Formula(operator.add, [in1, in2])
    ok(adder.state, unknown)
    adder.get_value(ValueSource())
    ok(adder.value, None)
    ok(adder.state, working)

    in1.send_value(5)
    ok(adder.value, None)
    ok(adder.state, working)

    in2.send_value(8)
    ok(adder.value, [13])
    ok(adder.state, known)

    in1.retract_value()
    ok(adder.value, None)
    ok(adder.state, unknown)

    in1.send_value(13)
    ok(adder.value, None)
    ok(adder.state, unknown)

    adder.get_value(ValueSource())
    ok(adder.value, [21])
    ok(adder.state, known)

    in1.retract_value()
    in2.retract_value()
    in1.send_value(3)
    ok(adder.value, None)
    ok(adder.state, unknown)

    in1.retract_value()
    in1.send_value(4)
    ok(adder.value, None)
    ok(adder.state, unknown)

    adder.get_value(ValueSource())
    ok(adder.value, None)
    ok(adder.state, working)

    in1.retract_value()
    ok(adder.state, working)
    in1.send_value(5)
    ok(adder.state, working)

    in2.send_value(6)
    ok(adder.state, known)
    ok(adder.value, [11])

test()

Reply via email to