diff --git a/src/policy/compile.py b/src/policy/compile.py index 2b12a5ff9..1b89e876d 100755 --- a/src/policy/compile.py +++ b/src/policy/compile.py @@ -115,6 +115,12 @@ class Atom (object): self.arguments = arguments self.location = location + @classmethod + def create_from_table_tuple(cls, table, tuple): + """ LIST is a python list representing an atom, e.g. + ['p', 17, "string", 3.14]. Returns the corresponding Atom. """ + return cls(table, [Term.create_from_python(x) for x in tuple]) + @classmethod def create_from_list(cls, list): """ LIST is a python list representing an atom, e.g. diff --git a/src/policy/runtime.py b/src/policy/runtime.py index 74823557d..bd95154d1 100644 --- a/src/policy/runtime.py +++ b/src/policy/runtime.py @@ -128,6 +128,9 @@ class Event(object): sign = '-' return "{}{}({})".format(self.table, sign, str(self.tuple)) + def atom(self): + return compile.Atom.create_from_table_tuple(self.table, self.tuple.tuple) + ############################################################################## ## Database ############################################################################## @@ -186,6 +189,18 @@ class Database(object): def __len__(self): return len(self.contents) + def __ge__(self, other): + return other <= self + + def __le__(self, other): + for proof in self.contents: + if proof not in other.contents: + return False + return True + + def __eq__(self, other): + return self <= other and other <= self + class DBTuple(object): def __init__(self, iterable, proofs=None): self.tuple = tuple(iterable) @@ -282,6 +297,23 @@ class Database(object): def log(self, table, msg, depth=0): self.tracer.log(table, "DB: " + msg, depth) + def is_noop(self, event): + """ Returns T if EVENT is a noop on the database. """ + # insert/delete same code but with flipped return values + # Code below is written as insert, except noop initialization. + if event.is_insert(): + noop = True + else: + noop = False + if event.table not in self.data: + return not noop + event_data = self.data[event.table] + for dbtuple in event_data: + # event.tuple is a dbtuple (and == only checks their .tuple fields) + if dbtuple == event.tuple and event.tuple.proofs <= dbtuple.proofs: + return noop + return not noop + def select(self, query): #logging.debug("DB: select({})".format(str(query))) if isinstance(query, compile.Atom): @@ -346,7 +378,7 @@ class Database(object): either [] meaning the lookup failed or [{}] meaning the lookup succeeded; otherwise, returns one binding list for each tuple in the database matching LITERAL under BINDING. """ - # slow--should stop at first match, not find all of them + # slow for negation--should stop at first match, not find all of them matches = self.matches_atom(literal, binding) if literal.is_negated(): if len(matches) > 0: @@ -622,6 +654,10 @@ class Runtime (object): """ Toplevel data evaluation routine. """ while len(self.queue) > 0: event = self.queue.dequeue() + if self.database.is_noop(event): + self.log(event.table, "is noop") + continue + self.log(event.table, "is not noop") if event.is_insert(): self.propagate(event) self.database.insert(event.table, event.tuple) @@ -683,6 +719,8 @@ class Runtime (object): for new_tuple in new_tuples: # self.log(event.table, # "new_tuple {}: {}".format(str(new_tuple), str(new_tuples[new_tuple]))) + # Only enqueue if new data. + # Putting the check here is necessary to support recursion. self.queue.enqueue(Event(table=atom.table, tuple=new_tuple, proofs=new_tuples[new_tuple], diff --git a/src/policy/tests/test_runtime.py b/src/policy/tests/test_runtime.py index ab3dbefcd..edef97ae6 100644 --- a/src/policy/tests/test_runtime.py +++ b/src/policy/tests/test_runtime.py @@ -440,6 +440,22 @@ class TestRuntime(unittest.TestCase): run.delete("p(x) :- q(x), r(x)") self.check(run, 'q(1) r(1) q(2) r(2)', "Delete rule") + def test_recursion(self): + run = self.prep_runtime('q(x,y) :- p(x,y)' + 'q(x,y) :- p(x,z), q(z,y)') + run.insert('p(1,2)') + run.insert('p(2,3)') + run.insert('p(3,4)') + run.insert('p(4,5)') + self.check(run, 'p(1,2) p(2,3) p(3,4) p(4,5)' + 'q(1,2) q(2,3) q(1,3) q(3,4) q(2,4) q(1,4) q(4,5) q(3,5) ' + 'q(1,5) q(2,5)', + 'Insert into recursive rules') + run.delete('p(1,2)') + self.check(run, 'p(2,3) p(3,4) p(4,5)' + 'q(2,3) q(3,4) q(2,4) q(4,5) q(3,5) q(2,5)', + 'Delete from recursive rules') + # TODO(tim): add tests for explanations def test_explanations(self): """ Test the explanation event handler. """