From c3ca6c8f922d508858171ea328e44f138e1e3dc7 Mon Sep 17 00:00:00 2001 From: Tim Vieira Date: Sat, 29 Jun 2013 22:49:42 -0400 Subject: [PATCH] Many changes * Support for `=` aggregator. #23 * BUGFIX: when --plan is specified (sorry :-X) * tweak to error handling. when an item's aggregator fails we set it's value to $error instead of it's last "ok" value. the repl also show this item as "changed" * tweaks to doctest runnner * attempt at contains/2 for list membership -- will eventually change to infix operator `in` * cons and nil can't be assigned values. --- src/Dyna/Backend/Python/Backend.hs | 2 +- src/Dyna/Backend/Python/chart.py | 27 +++++++-- src/Dyna/Backend/Python/defn.py | 38 +++++++++++++ src/Dyna/Backend/Python/dyna-doctest.py | 7 ++- src/Dyna/Backend/Python/errors.py | 3 + src/Dyna/Backend/Python/interpreter.py | 63 ++++++++++++++++++--- src/Dyna/Backend/Python/repl.py | 3 + src/Dyna/Backend/Python/term.py | 5 ++ test/repl/aggregator-conflict | 4 +- test/repl/late-aggregator-assignment.expect | 18 +++++- test/repl/retract-rule.expect | 6 +- 11 files changed, 154 insertions(+), 22 deletions(-) diff --git a/src/Dyna/Backend/Python/Backend.hs b/src/Dyna/Backend/Python/Backend.hs index 74d4fac..8d697d9 100644 --- a/src/Dyna/Backend/Python/Backend.hs +++ b/src/Dyna/Backend/Python/Backend.hs @@ -53,6 +53,7 @@ aggrs = S.fromList , "+=" , "*=" , "and=" , "or=" , "&=" , "|=" , ":-" + , "=" , "majority=" , "set=" , "bag=" , ":=" , "dict=" @@ -152,7 +153,6 @@ constants = go go ("split", _) = Just $ PDBS $ call "split" [] go ("float", _) = Just $ PDBS $ call "float" [] go ("int", _) = Just $ PDBS $ call "int" [] - go ("getattr", _) = Just $ PDBS $ call "getattr" [] go ("pycall", _) = Just $ PDBS $ call "pycall" [] go ("<=",2) = Just $ PDBS $ infixOp "<=" diff --git a/src/Dyna/Backend/Python/chart.py b/src/Dyna/Backend/Python/chart.py index 0b14aeb..7d3a9a8 100644 --- a/src/Dyna/Backend/Python/chart.py +++ b/src/Dyna/Backend/Python/chart.py @@ -17,17 +17,32 @@ class Chart(object): return aggregator(self.agg_name) def __repr__(self): - rows = [term for term in self.intern.values() if term.value is not None] - if not rows: return '' - if self.arity == 0: + [term] = rows return '%s := %s' % (term, _repr(term.value)) - - x = '\n'.join('%-30s := %s' % (term, _repr(term.value)) for term in sorted(rows)) - return '%s\n%s\n%s\n' % (self.name, '='*len(self.name), x) + p = [(_repr(term), _repr(term.value)) for term in sorted(rows)] + + lines = [self.name, '='*len(self.name)] # heading + + for term, value in p: + lines.append('%-30s := %s' % (term, value)) + +# terms, values = zip(*p) +# widths = map(len, terms) +# fmt = '%%-%ds => %%s.' % max(widths) +# if max(widths) > 50: +# for term, value in zip(terms, values): +# lines.append(term) +# lines.append(' => %s.' % value) +# else: +# for term, value in zip(terms, values): +# lines.append(fmt % (term, value)) + + lines.append('') + return '\n'.join(lines) def __getitem__(self, s): assert len(s) == self.arity + 1, \ diff --git a/src/Dyna/Backend/Python/defn.py b/src/Dyna/Backend/Python/defn.py index a45b35e..a67c4d2 100644 --- a/src/Dyna/Backend/Python/defn.py +++ b/src/Dyna/Backend/Python/defn.py @@ -3,10 +3,14 @@ from __future__ import division # TODO: codegen should produce specialized Term with inc/dec methods baked # in. This seems nicer than having a separate aggregator object. +# TODO: aggregators might want a reference to the item they are associated with. + import operator from collections import Counter from utils import drepr, _repr +from errors import AggregatorError +""" class Aggregator(object): def fold(self): raise NotImplementedError @@ -16,6 +20,24 @@ class Aggregator(object): raise NotImplementedError def clear(self): raise NotImplementedError +""" + +class NoAggregatorError(Exception): + """ + raised when an item doesn't have an aggregator. + """ + pass + + +class Aggregator(object): + def fold(self): + raise AggregatorError("item doesn't have an aggregator.") + def inc(self, _val, _ruleix, _variables): + pass + def dec(self, _val, _ruleix, _variables): + pass + def clear(self): + pass class BAggregator(Counter, Aggregator): @@ -53,6 +75,19 @@ class ColonEquals(BAggregator): return max(vs)[1] +class Equals(BAggregator): + def inc(self, val, _ruleix, _variables): + self[val] += 1 + def dec(self, val, _ruleix, _variables): + self[val] -= 1 + def fold(self): + vs = [v for v, cnt in self.iteritems() if cnt > 0] + if len(vs) != 1: + vs.sort() # for stability + raise AggregatorError('`=` got conflicting values %s' % (vs,)) + return vs[0] + + def user_vars(variables): "Post process the variables past to emit (which passes them to aggregator)." # remove the 'u' prefix on user variables 'uX' @@ -172,6 +207,9 @@ def aggregator(name): if name == ':=': return ColonEquals() + elif name == '=': + return Equals() + elif name == 'dict=': return DictEquals() diff --git a/src/Dyna/Backend/Python/dyna-doctest.py b/src/Dyna/Backend/Python/dyna-doctest.py index 0a9caf3..3d46866 100755 --- a/src/Dyna/Backend/Python/dyna-doctest.py +++ b/src/Dyna/Backend/Python/dyna-doctest.py @@ -7,7 +7,7 @@ from interpreter import Interpreter from repl import REPL from cStringIO import StringIO -from utils import red, green +from utils import red, green, strip_comments def extract(code): @@ -21,6 +21,9 @@ def run(code): repl = REPL(interp) for cmd, expect in extract(code): + if not cmd.strip(): + print + continue print ':-', cmd sys.stdout = x = StringIO() try: @@ -29,7 +32,7 @@ def run(code): sys.stdout = sys.__stdout__ got = x.getvalue().strip() expect = expect.strip() - if expect != got: + if strip_comments(expect) != strip_comments(got): print green % expect print red % got else: diff --git a/src/Dyna/Backend/Python/errors.py b/src/Dyna/Backend/Python/errors.py index d7ac76d..aed9e25 100644 --- a/src/Dyna/Backend/Python/errors.py +++ b/src/Dyna/Backend/Python/errors.py @@ -8,6 +8,9 @@ class DynaCompilerError(Exception): pass +class AggregatorError(Exception): + pass + class DynaInitializerException(Exception): def __init__(self, exception, init): rule = parse_attrs(init)['rule'] diff --git a/src/Dyna/Backend/Python/interpreter.py b/src/Dyna/Backend/Python/interpreter.py index f5fc997..ee04af9 100644 --- a/src/Dyna/Backend/Python/interpreter.py +++ b/src/Dyna/Backend/Python/interpreter.py @@ -19,10 +19,6 @@ TODO - sheebang? - - - TODO: @nwf remove comments from rule source - - - vbench: a script which tracks performace over time (= git commits). - profiler workflow @@ -139,7 +135,7 @@ from utils import ip, red, green, blue, magenta, yellow, parse_attrs, \ from prioritydict import prioritydict from config import dotdynadir -from errors import crash_handler, DynaInitializerException +from errors import crash_handler, DynaInitializerException, AggregatorError class Rule(object): @@ -165,6 +161,10 @@ class foo(dict): self.agg_name = agg_name super(foo, self).__init__() def __missing__(self, fn): + + if fn == 'contains/2': + return Contains() + arity = int(fn.split('/')[-1]) self[fn] = c = Chart(fn, arity, self.agg_name[fn]) return c @@ -174,7 +174,37 @@ def none(): return None -import os +class Contains(object): + + def __init__(self): + self.name = 'contains' + self.arity = 2 + + def __repr__(self): + return 'contains/2' + + def __getitem__(self, s): + assert len(s) == self.arity + 1, \ + 'Chart %r: item width mismatch: arity %s, item %s' % (self.name, self.arity, len(s)) + [x, xs], val = s[:-1], s[-1] + #assert val is True + if isinstance(x, slice): + assert not isinstance(xs, slice) + for a in xs.tolist(): + term = Term('contains/2', (a, xs)) + term.value = True + yield term, term.args, term.value + + else: + # all bound membership test + assert not isinstance(x, slice) and not isinstance(xs, slice) + term = Term('contains/2', (x, xs)) + term.value = (x in xs.tolist()) + yield term, term.args, term.value + + def insert(self, args): + assert False + class Interpreter(object): @@ -277,8 +307,14 @@ class Interpreter(object): print >> out def dump_rules(self): + if not self.rules: + return + print + print 'Rules' + print '=====' for i in sorted(self.rules): print '%3s: %s' % (i, self.rules[i].src) + print def build(self, fn, *args): # TODO: codegen should handle true/0 is True and false/0 is False @@ -345,9 +381,22 @@ class Interpreter(object): was = item.value try: now = item.aggregator.fold() + except AggregatorError as e: + error[item] = ('failed to aggregate item `%r` because %s' % (item, e), [(e, None)]) + + now = self.build('$error/0') + changed[item] = now + item.value = now + continue + except (ZeroDivisionError, TypeError, KeyboardInterrupt, NotImplementedError) as e: error[item] = ('failed to aggregate %r' % item.aggregator, [(e, None)]) + + now = self.build('$error/0') + changed[item] = now + item.value = now continue + if was == now: continue was_error = False @@ -603,7 +652,7 @@ def main(): if args.plan: # copy plan to tmp directory - plan = tmp / args.source.read_hexhash('sha1') + '.plan.py' + plan = interp.tmp / args.source.read_hexhash('sha1') + '.plan.py' args.source.copy(plan) else: diff --git a/src/Dyna/Backend/Python/repl.py b/src/Dyna/Backend/Python/repl.py index b7ba7d5..0cd8c62 100644 --- a/src/Dyna/Backend/Python/repl.py +++ b/src/Dyna/Backend/Python/repl.py @@ -57,6 +57,9 @@ class REPL(cmd.Cmd, object): :- c += a*b. :- rules + + Rules + ===== 0: a += 1. 1: b += 1. 2: c += a * b. diff --git a/src/Dyna/Backend/Python/term.py b/src/Dyna/Backend/Python/term.py index f0846a3..bc968e8 100644 --- a/src/Dyna/Backend/Python/term.py +++ b/src/Dyna/Backend/Python/term.py @@ -1,5 +1,7 @@ from errors import notimplemented from utils import _repr +from defn import Aggregator + # TODO: codegen should output a derived Term instance for each functor class Term(object): @@ -52,6 +54,7 @@ class Cons(Term): self.tail = tail assert isinstance(tail, (Cons, _Nil)), tail Term.__init__(self, 'cons/2', (head, tail)) + self.aggregator = Aggregator() def tolist(self): return [self.head] + self.tail.tolist() def __repr__(self): @@ -65,6 +68,8 @@ class Cons(Term): class _Nil(Term): def __init__(self): Term.__init__(self, 'nil/0', ()) + self.aggregator = Aggregator() + def tolist(self): return [] def __repr__(self): diff --git a/test/repl/aggregator-conflict b/test/repl/aggregator-conflict index 31b7f66..fb02378 100755 --- a/test/repl/aggregator-conflict +++ b/test/repl/aggregator-conflict @@ -4,6 +4,6 @@ echo -e " a += 1. a." |./dyna > $0.out -diff <(sed -e 's/[ -][^ -]*\/\.dyna/ /g' $0.expect) \ - <(sed -e 's/[ -][^ -]*\/\.dyna/ /g' $0.out) \ +diff <(sed -e 's/[ -][^ -]*\.dyna/ /g' $0.expect) \ + <(sed -e 's/[ -][^ -]*\.dyna/ /g' $0.out) \ && echo pass diff --git a/test/repl/late-aggregator-assignment.expect b/test/repl/late-aggregator-assignment.expect index 0599ae3..a5e4486 100644 --- a/test/repl/late-aggregator-assignment.expect +++ b/test/repl/late-aggregator-assignment.expect @@ -1,12 +1,24 @@ -:- :- :- 0: a += b * c. +:- :- :- +Rules +===== + 0: a += b * c. + :- ============= b := 2 -:- 0: a += b * c. +:- +Rules +===== + 0: a += b * c. 1: b := 2. + :- ============= a := 6 c := 3 -:- 0: a += b * c. +:- +Rules +===== + 0: a += b * c. 1: b := 2. 2: c := 3. + :- exit diff --git a/test/repl/retract-rule.expect b/test/repl/retract-rule.expect index bfc109b..001c5f4 100644 --- a/test/repl/retract-rule.expect +++ b/test/repl/retract-rule.expect @@ -4,9 +4,13 @@ a := 1 b := 1 :- ============= a := 2 -:- 0: a += 1. +:- +Rules +===== + 0: a += 1. 1: b += 1. 2: a += 1. + :- Solution ======== -- 2.50.1