From: timv Date: Wed, 12 Jun 2013 01:46:57 +0000 (-0400) Subject: added test/repl X-Git-Url: https://hydra-www.ietfng.org/gitweb/?a=commitdiff_plain;h=99fca94c8ddd946827c077eb772157a0a47a3452;p=dyna2 added test/repl Change codegen output slightly for easier loading. Moved Term into it's own module. Preliminary support for unification. added --profile flag. changed output of rules command at the repl. repl no longer dies if an initializer exception is thrown (initializer exceptions throw out newly added rules). --- diff --git a/src/Dyna/Backend/Python/Backend.hs b/src/Dyna/Backend/Python/Backend.hs index 85d599a..a6e457c 100644 --- a/src/Dyna/Backend/Python/Backend.hs +++ b/src/Dyna/Backend/Python/Backend.hs @@ -273,11 +273,14 @@ printPlanHeader r c mn = do printInitializer :: Handle -> Rule -> Cost -> Actions PyDopeBS -> IO () printInitializer fh rule@(Rule _ h _ r _ _ ucruxes _) cost dope = do displayIO fh $ renderPretty 1.0 100 - $ "@_initializers.append" -- <> (uncurry pfa $ MA.fromJust $ findHeadFA h ucruxes) - `above` "def" <+> char '_' <> tupled ["emit"] <> colon + $ "def" <+> char '_' <> tupled ["emit"] <> colon `above` (indent 4 $ printPlanHeader rule cost Nothing) `above` pdope dope <> line + <> "_initializers.append((" <> (pretty $ r_index rule) <> ", _" <> "))" + <> line + <> line + <> line -- XXX INDIR EVAL printUpdate :: Handle -> Rule -> Cost -> Int -> Maybe DFunctAr -> (DVar, DVar) -> Actions PyDopeBS -> IO () @@ -288,7 +291,7 @@ printUpdate fh rule@(Rule _ h _ r _ _ _ _) cost evalix (Just (f,a)) (hv,v) dope `above` (indent 4 $ printPlanHeader rule cost (Just evalix)) `above` pdope dope <> line - <> "_updaters.append((" <> (pfa f a) <> ", _))" + <> "_updaters.append((" <> (pfa f a) <> "," <> (pretty $ r_index rule) <> ",_))" <> line <> line <> line diff --git a/src/Dyna/Backend/Python/chart.py b/src/Dyna/Backend/Python/chart.py index a9a8b0c..b2fb50c 100644 --- a/src/Dyna/Backend/Python/chart.py +++ b/src/Dyna/Backend/Python/chart.py @@ -1,57 +1,7 @@ from collections import defaultdict from utils import notimplemented - -# TODO: codegen should output a derived Term instance for each functor -class Term(object): - - __slots__ = 'fn args value aggregator'.split() - - def __init__(self, fn, args): - self.fn = fn - self.args = args - self.value = None - self.aggregator = None - - def __cmp__(self, other): - if other is None: - return 1 - if not isinstance(other, Term): - return 1 - return cmp((self.fn, self.args), (other.fn, other.args)) - - # default hash and eq suffice because we intern - #def __hash__(self): - #def __eq__(self): - - def __repr__(self): - "Pretty print a term. Will retrieve the complete (ground) term." - fn = '/'.join(self.fn.split('/')[:-1]) # drop arity from name. - if not self.args: - return fn - return '%s(%s)' % (fn, ','.join(map(_repr, self.args))) - - def __getstate__(self): - return (self.fn, self.args, self.value, self.aggregator) - - def __setstate__(self, state): - (self.fn, self.args, self.value, self.aggregator) = state - - __add__ = __sub__ = __mul__ = notimplemented - - -def _repr(x): - if x is True: - return 'true' - elif x is False: - return 'false' - elif x is None: - return 'null' - elif isinstance(x, basestring): - # dyna doesn't accept single-quoted strings - return '"%s"' % x.replace('"', r'\"') - else: - return repr(x) +from term import Term, _repr class Chart(object): diff --git a/src/Dyna/Backend/Python/graph.py b/src/Dyna/Backend/Python/graph.py index aa0f213..edd64ae 100644 --- a/src/Dyna/Backend/Python/graph.py +++ b/src/Dyna/Backend/Python/graph.py @@ -30,7 +30,7 @@ def animate(interp): ax = pl.axes() print 'creating animation..' - anim = FuncAnimation(fig, lambda t: g(nodes, edges, t % niter, ax, interp), frames=niter*3) + anim = FuncAnimation(fig, lambda t: g(nodes, edges, t % niter, ax, interp), frames=niter) print 'saving...' anim.save('examples/force.dyna.mp4', fps=30, extra_args=['-vcodec', 'libx264']) print 'wrote examples/force.dyna.mp4' diff --git a/src/Dyna/Backend/Python/interpreter.py b/src/Dyna/Backend/Python/interpreter.py index 70c9185..38247ba 100644 --- a/src/Dyna/Backend/Python/interpreter.py +++ b/src/Dyna/Backend/Python/interpreter.py @@ -8,6 +8,13 @@ TODO - profiler workflow + kcachegrind + $ (PYTHONPATH=src/Dyna/Backend/Python/ pycachegrind src/Dyna/Backend/Python/interpreter.py examples/papa.dyna) + + cProfile + snakeviz + $ python -m cProfile -o prof src/Dyna/Backend/Python/interpreter.py examples/force.dyna >/dev/null && snakeviz prof + + - unit tests and code coverage. - doc tests for Dyna code. @@ -53,6 +60,8 @@ FASTER - TODO: BAggregators aren't very efficient. + - interning with integers instead of deduplicated instances of Term. + STRONGER (robustness) ===================== @@ -107,6 +116,8 @@ BUGS few things -- I think assertionerror is one them... we should probably do whatever this is doing with a custom exception. + - TODO: some items only learn their aggregators late in life... + NOTES ===== @@ -204,10 +215,10 @@ class Rule(object): self.updaters = [] @property def span(self): - return self.init.dyna_attrs['Span'] + return parse_attrs(self.init)['Span'] @property def src(self): - return self.init.dyna_attrs['rule'] + return parse_attrs(self.init)['rule'] def __repr__(self): return 'Rule(%s, %r)' % (self.idx, self.src) @@ -223,8 +234,6 @@ class Interpreter(object): self.agenda = prioritydict() self.parser_state = '' - self.tape = [] - def newchart(fn): arity = int(fn.split('/')[-1]) return Chart(fn, arity, lambda: aggregator(self.agg_name[fn])) @@ -281,8 +290,8 @@ class Interpreter(object): print >> out def dump_rules(self): - for i in sorted(self.rules, key=int): - print self.rules[i] + for i in sorted(self.rules): + print '%3s: %s' % (i, self.rules[i].src) def build(self, fn, *args): # TODO: codegen should handle true/0 is True and false/0 is False @@ -324,7 +333,6 @@ class Interpreter(object): def retract_rule(self, idx): "Retract rule and all of it's edges." - assert isinstance(idx, str) try: rule = self.rules.pop(idx) except KeyError: @@ -353,17 +361,11 @@ class Interpreter(object): def _go(self): "the main loop" - - tape = self.tape - changed = {} agenda = self.agenda errors = self.errors while agenda: item = agenda.pop_smallest() - - tape.append(item) - was = item.value try: now = item.aggregator.fold() @@ -417,18 +419,17 @@ class Interpreter(object): # this is not possible. self.emit(*e) - def new_updater(self, fn, handler): + def new_updater(self, fn, ruleix, handler): self.updaters[fn].append(handler) - i = handler.dyna_attrs['RuleIx'] - rule = self.rules[i] + rule = self.rules[ruleix] rule.updaters.append(handler) - handler.dyna_rule = rule + handler.rule = rule - def new_initializer(self, init): - i = init.dyna_attrs['RuleIx'] - rule = self.rules[i] + def new_initializer(self, ruleix, init): + rule = self.rules[ruleix] assert rule.init is None rule.init = init + init.rule = rule def delete_emit(self, item, val, ruleix, variables): self.emit(item, val, ruleix, variables, delete=True) @@ -477,14 +478,10 @@ class Interpreter(object): for k, v in env['_agg_decl'].items(): self.new_fn(k, v) - for fn, h in env['_updaters']: - h.dyna_attrs = parse_attrs(h) - for h in env['_initializers']: - h.dyna_attrs = parse_attrs(h) try: # only run new initializers - for init in env['_initializers']: + for _, init in env['_initializers']: init(emit=_emit) except (TypeError, ZeroDivisionError) as e: @@ -496,11 +493,11 @@ class Interpreter(object): # in the middle of the following blocK? # add new updaters - for fn, h in env['_updaters']: - self.new_updater(fn, h) + for fn, r, h in env['_updaters']: + self.new_updater(fn, r, h) # add new initializers - for h in env['_initializers']: - self.new_initializer(h) + for r, h in env['_initializers']: + self.new_initializer(r, h) # accept the new parser state self.parser_state = env['parser_state'] # process emits @@ -564,44 +561,84 @@ def main(): parser.add_argument('--postprocess', type=file, help='run post-processing script.') - argv = parser.parse_args() + parser.add_argument('--profile', action='store_true', + help='run profiler.') + + args = parser.parse_args() interp = Interpreter() - if argv.source: + if args.profile: + # When profiling, its common practice to disable the garbage collector. + import gc + gc.disable() + + # However, it's worth noting that the garbage collector is a real part + # of the runtime. Some implementation details will change how often it + # runs. For example, when we call emit with the variable binding + # dictionary this increases refcounts to a bunch of variables. For some + # reason the garbage collector overhead is lowered in this + # situation. Giving in a counterintuitive result. + + import cProfile + + cmd = 'interp.do(plan)' + plan = '%s.plan.py' % args.source + + dynac(args.source, plan) + + p = cProfile.Profile() + + # redirect stdout + #sys.stdout = file(args.source + '.stdout', 'wb') + + p.runctx(cmd, globals(), locals()) + p.dump_stats('prof') + + interp.dump_charts() + + # call graph + os.system('gprof2dot.py -f pstats prof | dot -Tsvg -o prof.svg && eog prof.svg &') + os.system('snakeviz prof &') + + return + + + if args.source: - if not os.path.exists(argv.source): - print 'File %r does not exist.' % argv.source + if not os.path.exists(args.source): + print 'File %r does not exist.' % args.source return - if argv.plan: - plan = argv.source + if args.plan: + plan = args.source else: - plan = "%s.plan.py" % argv.source - dynac(argv.source, plan) + plan = "%s.plan.py" % args.source + dynac(args.source, plan) interp.do(plan) - if argv.output: - if argv.output == "-": + if args.output: + if args.output == "-": interp.dump_charts(sys.stdout) else: - with file(argv.output, 'wb') as f: + with file(args.output, 'wb') as f: interp.dump_charts(f) else: interp.dump_charts() - if argv.interactive: - interp.repl(hist = argv.source + '.hist') + if args.interactive: + interp.repl(hist = args.source + '.hist') else: interp.repl(hist = '/tmp/dyna.hist') - if argv.draw: + if args.draw: interp.draw() - if argv.postprocess is not None: - execfile(argv.postprocess.name, {'interp': interp}) + if args.postprocess is not None: + # TODO: import and call main method instead. + execfile(args.postprocess.name, {'interp': interp}) if __name__ == '__main__': diff --git a/src/Dyna/Backend/Python/repl.py b/src/Dyna/Backend/Python/repl.py index 89ed971..a3ea9b7 100644 --- a/src/Dyna/Backend/Python/repl.py +++ b/src/Dyna/Backend/Python/repl.py @@ -1,7 +1,7 @@ import os, sys import cmd, readline import interpreter -from utils import blue, yellow, green, magenta, ip, DynaCompilerError, AggregatorConflict +from utils import blue, yellow, green, magenta, ip, DynaCompilerError, AggregatorConflict, DynaInitializerException from chart import _repr from config import dotdynadir import debug @@ -29,7 +29,7 @@ class REPL(cmd.Cmd, object): self.interp.dump_rules() def do_retract_rule(self, idx): - self.interp.retract_rule(idx) + self.interp.retract_rule(int(idx)) def do_retract_item(self, item): self.interp.retract_item(item) @@ -111,12 +111,8 @@ class REPL(cmd.Cmd, object): src = self.interp.dynac_code(line) # might raise DynaCompilerError changed = self.interp.do(src) # throws AggregatorConflict - except AggregatorConflict as e: - print 'AggregatorConflict:', e - print '> new rule(s) were not added to program.' - - except DynaCompilerError as e: - print 'DynaCompilerError:' + except (AggregatorConflict, DynaInitializerException, DynaCompilerError) as e: + print type(e), ':' print e print '> new rule(s) were not added to program.' diff --git a/src/Dyna/Backend/Python/term.py b/src/Dyna/Backend/Python/term.py new file mode 100644 index 0000000..dd88c0c --- /dev/null +++ b/src/Dyna/Backend/Python/term.py @@ -0,0 +1,239 @@ +from utils import notimplemented + +# TODO: codegen should output a derived Term instance for each functor +class Term(object): + + __slots__ = 'fn args value aggregator'.split() + + def __init__(self, fn, args): + self.fn = fn + self.args = args + self.value = None + self.aggregator = None + + def __cmp__(self, other): + if other is None: + return 1 + if not isinstance(other, Term): + return 1 + return cmp((self.fn, self.args), (other.fn, other.args)) + + # default hash and eq suffice because we intern + #def __hash__(self): + #def __eq__(self): + + def __repr__(self): + "Pretty print a term. Will retrieve the complete (ground) term." + fn = '/'.join(self.fn.split('/')[:-1]) # drop arity from name. + if not self.args: + return fn + return '%s(%s)' % (fn, ','.join(map(_repr, self.args))) + + def __getstate__(self): + return (self.fn, self.args, self.value, self.aggregator) + + def __setstate__(self, state): + (self.fn, self.args, self.value, self.aggregator) = state + + __add__ = __sub__ = __mul__ = notimplemented + + def subst(self, v): + if self in v: + return v[self] + # TODO: this should go thru the Chart + return Term(self.fn, tuple(x if isconst(x) else x.subst(v) for x in self.args)) + + +def _repr(x): + if x is True: + return 'true' + elif x is False: + return 'false' + elif x is None: + return 'null' + elif isinstance(x, basestring): + # dyna doesn't accept single-quoted strings + return '"%s"' % x.replace('"', r'\"') + else: + return repr(x) + + +def isconst(x): + return not isinstance(x, (Variable, Term)) + + +class Variable(object): + + def __init__(self, name): + self.fn = name + self._val = None + + @property + def value(self): + return self.root._val + + @property + def root(self): + if isinstance(self._val, Variable): + return self._val.root + else: + return self + + @value.setter + def value(self, v): + self._val = v + +# def __repr__(self, other): +# if self.value is None: +# return self.fn + '=' + self.root.fn +# else: +# return _repr(self.value) +# return '%s=%s' % (self.fn, _repr(self.value)) + + def __repr__(self): + if self.value is None: + return self.root.fn + else: + return _repr(self.value) + + def __eq__(self, other): + if isinstance(other, Variable): + + if self.root is other.root: + return True + + if self.value is not None: + return other.value == self.value + + return False + + else: + return other == self.value + + def subst(self, v): + if self in v: + return v[self] + return self + + +def extend(v, x, t): + """ + Extend valuation v with v[x] = t + """ + v1 = v.copy() + v1[x] = t + x.value = t + return v1 + + +def occurs_check(v, t): + """ + Return true if variable ``v`` occurs anywhere in term ``t``." + + >>> [f,g,h,X,Y,Z] = map(symbol, ['f','g','h','X','Y','Z']) + + >>> occurs_check(X, f(g(h(X,Y),X))) + True + + >>> occurs_check(Z, f(g(h(X,Y),X))) + False + + """ + if v == t: + return True + elif isinstance(t, Term): + return any(occurs_check(v, x) for x in t.args) + return False + + +def unify_var(x, t, v): + """ + Test if v can be extended with v[x] = t; + In that case return the extention + Else return None + """ + if x in v: + return _unify(v[x], t, v) + elif occurs_check(x, t): + return None + else: + return extend(v, x, t) + + +def unify(x,y): + return _unify(x,y,{}) + + +def _unify(x,y,v): + """ + Find one valuation extending v and unifying x with y + """ + if v is None: + return None + elif x == y: + return v + elif isinstance(x, Variable): + return unify_var(x, y, v) + elif isinstance(y, Variable): + return unify_var(y, x, v) + elif isinstance(x, Term) and isinstance(y, Term) and x.fn == y.fn: + if len(x.args) == len(y.args): + for a, b in zip(x.args, y.args): + v = _unify(a, b, v) + return v + + +def symbol(name): + if name[0].isupper(): + return Variable(name) + + class Symbol(object): + def __init__(self, name): + self.name = name + def __call__(self, *args): + return Term('%s/%s' % (self.name, len(args)), args) + def __str__(self): + return self.name + + return Symbol(name) + + +if __name__ == '__main__': + + from utils import ip + + [f,g,h] = map(symbol, ['f','g','h']) + vs = [X,Y,Z] = map(symbol, ['X','Y','Z']) + + def test(a, b): + for v in vs: + v.value = None + + print + print 'unify %s and %s' % (a,b) + s = unify(a, b) + print '->', s + if s is None: + return + + print a, b + assert a == b + assert repr(a) == repr(b), [a,b] + + for v in vs: + v.value = None + + test(f(X), f(g(h(X,Y),X))) + test(f(X), f(g(h(Z),"foo"))) + test(f(X, Y), f("cat", 123)) + test(f(X), f(X)) + test(f(X), f(Y)) + + # TODO: this fails: + #test("", "") + + Z.value = 3 + Y.value = Z + X.value = Y + print [X,Y,Z] + assert X.value == Y.value == Z.value == 3 diff --git a/src/Dyna/Backend/Python/utils.py b/src/Dyna/Backend/Python/utils.py index fed7af0..4a35be7 100644 --- a/src/Dyna/Backend/Python/utils.py +++ b/src/Dyna/Backend/Python/utils.py @@ -27,8 +27,8 @@ class DynaInitializerException(Exception): def __init__(self, exception, init): msg = '%r in ininitializer for rule\n %s\n %s' % \ (exception, - init.dyna_attrs['Span'], - init.dyna_attrs['rule']) + parse_attrs(init)['Span'], + parse_attrs(init)['rule']) super(DynaInitializerException, self).__init__(msg) diff --git a/test/repl/aggregator-conflict b/test/repl/aggregator-conflict new file mode 100755 index 0000000..3fd4ee5 --- /dev/null +++ b/test/repl/aggregator-conflict @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +echo -e " +a += 1. +a." |./dyna > $0.out + +colordiff $0.expect $0.out && echo pass diff --git a/test/repl/aggregator-conflict.expect b/test/repl/aggregator-conflict.expect new file mode 100644 index 0000000..3238426 --- /dev/null +++ b/test/repl/aggregator-conflict.expect @@ -0,0 +1,6 @@ +:- :- ============= +a := 1 + +:- AggregatorConflict: Aggregator conflict 'a/0' was '+=' trying to set to '|='. +> new rule(s) were not added to program. +:- exit diff --git a/test/repl/retract-rule b/test/repl/retract-rule new file mode 100755 index 0000000..e96b1c2 --- /dev/null +++ b/test/repl/retract-rule @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +echo -e " +a += 1. +b += 1. +a += 1. +rules +chart +retract_rule 0 +retract_rule 1 +chart" |./dyna > $0.out + +colordiff $0.expect $0.out && echo pass + +# TODO: add test to make sure that rule retraction will clear errors. + +#$ echo -e "retract_rule 0\n retract_rule 3\nretract_rule 6\nrules\nchart" | ./dyna examples/errors.dyna -i diff --git a/test/repl/retract-rule.expect b/test/repl/retract-rule.expect new file mode 100644 index 0000000..1552807 --- /dev/null +++ b/test/repl/retract-rule.expect @@ -0,0 +1,35 @@ +:- :- ============= +a := 1 + +:- ============= +b := 1 + +:- ============= +a := 2 + +:- 0: + 1: a += 1. + 2: a += 1. +:- +Charts +============ +a/0 +================= +a := 2 + +b/0 +================= +b := 1 + +:- :- :- +Charts +============ +a/0 +================= +a := 1 + +b/0 +================= + + +:- exit