From df6c7f40e8dab33ba7739252c072bfdd6dc15c51 Mon Sep 17 00:00:00 2001 From: Tim Vieira Date: Tue, 16 Jul 2013 14:26:25 -0400 Subject: [PATCH] Dropped end-to-end tests from haskell test runner (removed support for --plan flag for ./dyna). These test are now part of python test runnner. Minor refactoring/reorganization of interpreter and repl. --- examples/dijkstra-backpointers.dyna | 2 +- run-doctests.py | 53 ++- src/Dyna/Backend/Python/Selftest.hs | 99 ----- src/Dyna/Backend/Python/interpreter.py | 541 +++++++++++------------ src/Dyna/Backend/Python/load/__init__.py | 2 +- src/Dyna/Backend/Python/main.py | 39 +- src/Dyna/Backend/Python/repl.py | 149 ++----- src/Dyna/Main/TestsDriver.hs | 4 +- test/repl/retract-bc-2.dynadoc | 96 ++++ 9 files changed, 465 insertions(+), 520 deletions(-) delete mode 100644 src/Dyna/Backend/Python/Selftest.hs create mode 100644 test/repl/retract-bc-2.dynadoc diff --git a/examples/dijkstra-backpointers.dyna b/examples/dijkstra-backpointers.dyna index 68f5f97..68b8fec 100644 --- a/examples/dijkstra-backpointers.dyna +++ b/examples/dijkstra-backpointers.dyna @@ -1,6 +1,6 @@ % Single source shortest path with optimal path extraction. -% Cost of the optimal path from start to each node. +% Cost of the optimal path from start to each node. path(start) min= 0 with_key []. path(B) min= path(A) + edge(A,B) with_key A. diff --git a/run-doctests.py b/run-doctests.py index cb4f732..32ecb74 100755 --- a/run-doctests.py +++ b/run-doctests.py @@ -1,18 +1,52 @@ #!/usr/bin/env python from path import path from cStringIO import StringIO +from subprocess import Popen, PIPE -import sys -sys.path.append('src/Dyna/Backend/Python') +import re, sys +src = 'src/Dyna/Backend/Python' +sys.path.append(src) -print -print 'Doctests' -print '========' +if not path(src).exists(): + print >> sys.stderr, 'Tests must be run in top level directory.' + exit(1) -from dyna_doctest import run from utils import red, green +from dyna_doctest import run failures = [] + +print 'End-to-end' +print '==========' + +for z in path('examples/expected').glob("*.py.out"): + + x = re.sub('(examples/)expected/(.*).py.out', r'\1\2.dyna', z) + y = x + '.py.out' + + print x, + sys.stdout.flush() + + # run ./dyna + p = Popen(['./dyna', x, '-o', y], stdout=PIPE, stderr=PIPE) + (out, err) = p.communicate() + + assert not p.returncode, out + '\n' + err + + # look at diff + p = Popen(['diff', y, z], stdout=PIPE, stderr=PIPE) + (out, err) = p.communicate() + + if not p.returncode: + print green % 'pass' + else: + print red % 'fail' + failures.append([x, out + '\n' + err]) + +print +print 'Doctests' +print '========' + for x in path('test').glob("*/*.dynadoc"): print x, sys.stdout.flush() @@ -24,6 +58,7 @@ for x in path('test').glob("*/*.dynadoc"): else: print green % 'pass' + for f, log in failures: print print '================================================' @@ -31,5 +66,11 @@ for f, log in failures: print '================================================' print log + if failures: + print '================================================' + print 'FAILURES (%s)' % len(failures) + print '================================================' + for f, _ in failures: + print f exit(1) diff --git a/src/Dyna/Backend/Python/Selftest.hs b/src/Dyna/Backend/Python/Selftest.hs deleted file mode 100644 index 19385e8..0000000 --- a/src/Dyna/Backend/Python/Selftest.hs +++ /dev/null @@ -1,99 +0,0 @@ ---------------------------------------------------------------------------- --- | Self-tests for the Python backend, mostly by running the generated --- code through the interpreter. - --- Header material {{{ -{-# LANGUAGE ImplicitParams #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -module Dyna.Backend.Python.Selftest where - -import Control.Exception (handle,throw) -import qualified Dyna.Backend.Python.Backend as DP -import qualified Dyna.Main.Driver as D -import System.Directory (removeFile) -import System.Exit (ExitCode(..)) -import System.IO -import System.IO.Error -import System.Process -import Test.Framework as TF -import Test.Framework.TH -import Test.Golden - -------------------------------------------------------------------------}}} --- Run Backend {{{ - -runDynaPy :: FilePath -> FilePath -> FilePath -> IO () - --- XXX this 'handle' thing is pretty hackish, but it does stop us from --- breaking the test harness. -runDynaPy f pl out = handle (\(_ :: ExitCode) -> return ()) $ do - _ <- tryIOError $ removeFile pl - _ <- tryIOError $ removeFile out - - let ?dcfg = D.defaultDynacConfig - { D.dcfg_backend = DP.pythonBackend - , D.dcfg_outFile = Just pl - } - in D.processFile f - - withFile "/dev/null" ReadWriteMode $ \devnull -> do - (Nothing,Nothing,Nothing,ph) <- createProcess $ CreateProcess - { cmdspec = RawCommand "/usr/bin/env" - [ "python" - , "src/Dyna/Backend/Python/main.py" - , "--plan" - , "-o", out - , pl - ] - , cwd = Nothing - , env = Nothing - , std_in = UseHandle devnull - , std_out = UseHandle devnull - , std_err = UseHandle devnull - , close_fds = True - , create_group = False - } - ec <- waitForProcess ph - _ <- tryIOError $ removeFile pl - case ec of - ExitSuccess -> return () - ExitFailure _ -> throw ec - -------------------------------------------------------------------------}}} --- Tests {{{ - -mkExample :: String -> TF.Test -mkExample name = - let (dy,pl,out,ex) = names in goldenVsFile dy ex out (runDynaPy dy pl out) - where - names = ( "examples/" ++ name ++ ".dyna" - , "examples/" ++ name ++ ".dyna.py.plan" - , "examples/" ++ name ++ ".dyna.py.out" - , "examples/expected/" ++ name ++ ".py.out") - --- Sorted roughly by likelihood that all subsequent examples --- will be broken. ;) -test_End_To_End :: [Test] -test_End_To_End = map mkExample - [ "simple", "equalities", "fib-limit", "dijkstra", "papa2", "matrixops" - , "factorial-bc", "geom", "dijkstra-backpointers" ] - ---test_REPL :: [Test] ---test_REPL = map (\n -> testProgramRuns n ("./test/repl/"++n) []) --- [ "aggregator-conflict" --- , "retract-rule" --- , "late-aggregator-assignment" ] - -------------------------------------------------------------------------}}} --- Harness toplevel {{{ - -selftest :: TF.Test -selftest = $(testGroupGenerator) - --- If you're running from within GHCi and just want to do something quickly, --- try --- --- TF.defaultMain [mkExample "simple"] - -------------------------------------------------------------------------}}} diff --git a/src/Dyna/Backend/Python/interpreter.py b/src/Dyna/Backend/Python/interpreter.py index 2bdafca..587e4ee 100644 --- a/src/Dyna/Backend/Python/interpreter.py +++ b/src/Dyna/Backend/Python/interpreter.py @@ -26,22 +26,11 @@ class Rule(object): self.init = None self.updaters = [] self.query = None - self._span = None - self._src = None + self.span = None + self.src = None self.initialized = False - - @property - def span(self): - if self._span is None: - span = parse_attrs(self.init or self.query)['Span'] - self._span = hide_ugly_filename(span) - return self._span - - @property - def src(self): - if self._src is None: - self._src = strip_comments(parse_attrs(self.init or self.query)['rule']) - return self._src + self.anf = None + self.head_fn = None def __repr__(self): return 'Rule(%s, %r)' % (self.index, self.src) @@ -77,7 +66,7 @@ class Interpreter(object): self.parser_state = '' self.files = [] # rules - self.rules = ddict(Rule) + self.rules = {} self.updaters = defaultdict(list) self._gbc = defaultdict(list) # data structures @@ -112,116 +101,6 @@ class Interpreter(object): # check for aggregator conflict. assert self.agg_name[fn] == agg, (fn, self.agg_name[fn], agg) - def dump_charts(self, out=None): - if out is None: - out = sys.stdout - fns = self.chart.keys() - fns.sort() - fns = [x for x in fns if x not in self._gbc] # don't show backchained items - nullary = [x for x in fns if x.endswith('/0')] - others = [x for x in fns if not x.endswith('/0')] - - # show nullary charts first - nullary = filter(None, [str(self.chart[x]) for x in nullary]) - charts = filter(None, [str(self.chart[x]) for x in others if not x.startswith('$rule/')]) - - if nullary or charts: - print >> out - print >> out, 'Solution' - print >> out, '========' - else: - print >> out, 'Solution empty.' - - if nullary: - for line in nullary: - print >> out, line - print >> out - for line in charts: - print >> out, line - print >> out - - self.dump_errors(out) - - def dump_errors(self, out=None): - if out is None: - out = sys.stdout - # We only dump the error chart if it's non empty. - if not self.error and not self.uninitialized_rules: - return - print >> out - print >> out, red % 'Errors' - print >> out, red % '======' - - # separate errors into aggregation errors and update handler errors - I = defaultdict(lambda: defaultdict(list)) - E = defaultdict(lambda: defaultdict(list)) - for item, (val, es) in self.error.items(): - for e, h in es: - if h is None: - I[item.fn][type(e)].append((item, val, e)) - else: - E[h.rule][type(e)].append((item, val, e)) - - # aggregation errors - for r in sorted(I, key=lambda r: r.index): - print >> out, 'Error(s) aggregating %s:' % r - for etype in I[r]: - print >> out, ' %s:' % etype.__name__ - for i, (item, value, e) in enumerate(sorted(I[r][etype])): - if i >= 5: - print >> out, ' %s more ...' % (len(I[r][etype]) - i) - break - print >> out, ' `%s`: %s' % (item, e) - print >> out - - # errors pertaining to rules - for r in sorted(E, key=lambda r: r.index): - print >> out, 'Error(s) in rule:', r.span - print >> out - for line in r.src.split('\n'): - print >> out, ' ', line - print >> out - for etype in E[r]: - print >> out, ' %s:' % etype.__name__ - for i, (item, value, e) in enumerate(sorted(E[r][etype])): - if i >= 5: - print >> out, ' %s more ...' % (len(E[r][etype]) - i) - break - print >> out, ' when `%s` = %s' % (item, _repr(value)) - print >> out, ' %s' % (e) - print >> out - print >> out, r.render_ctx(e.exception_frame, indent=' ') - print >> out - - # uninitialized rules - if self.uninitialized_rules: - print >> out, red % 'Uninitialized rules' - print >> out, red % '===================' - for e, r in self.uninitialized_rules: - print >> out, 'Failed to initialize rule:' - rule = self.rules[r] - print >> out, ' ', rule.src - print >> out, ' due to `%s`' % e - print >> out, rule.render_ctx(e.exception_frame, indent=' ') - print >> out - - print >> out - - def dump_rules(self): - if not self.rules: - print 'No rules found.' - return - print - print 'Rules' - print '=====' - for i in sorted(self.rules): - rule = self.rules[i] - if rule.init is not None and not rule.initialized: - print '%3s: %s <-- uninitialized' % (i, rule.src) - else: - print '%3s: %s' % (i, rule.src) - print - def build(self, fn, *args): # handle a few special cases where the item doesn't have a chart if fn == 'cons/2': @@ -238,71 +117,18 @@ class Interpreter(object): self.new_fn(fn, None) return self.chart[fn].insert(args) - def retract_rule(self, idx): - "Retract rule and all of it's edges." - - try: - rule = self.rules.pop(idx) - except KeyError: - print 'Rule %s not found.' % idx - return - - # remove $rule - if hasattr(rule, 'item'): - self.delete_emit(rule.item, true, ruleix=None, variables=None) - - uninits = [(e, r) for e, r in self.uninitialized_rules if r != idx] - if len(uninits) != len(self.uninitialized_rules): - self.uninitialized_rules = uninits - return [] + def delete_emit(self, item, val, ruleix, variables): + self.emit(item, val, ruleix, variables, delete=True) - if rule.init is not None: - # Forward chained rule -- - # remove update handlers - for u in rule.updaters: - for xs in self.updaters.values(): - if u in xs: - xs.remove(u) - assert u not in xs, 'Several occurrences of u in xs' - if rule.initialized: - # run initializer in delete mode - try: - rule.init(emit=self.delete_emit) - except (ZeroDivisionError, TypeError, KeyboardInterrupt, RuntimeError, OverflowError): - # TODO: what happens if there's an error? - pass + def emit(self, item, val, ruleix, variables, delete): + if delete: + item.aggregator.dec(val, ruleix, variables) else: - # Backchained rule -- - # remove query handler - self._gbc[rule.head_fn].remove(rule.query) - # blast the memo entries for items this rule may have helped derive. - if rule.head_fn in self.chart: - - # update values before propagating - for head in self.chart[rule.head_fn].intern.itervalues(): - def _emit(item, val, ruleix, variables): - item.aggregator.dec(val, ruleix, variables) - try: - rule.query(*head.args, emit=_emit) - except (ZeroDivisionError, TypeError, KeyboardInterrupt, RuntimeError, OverflowError): - # TODO: what happens if there's an error? - pass - - # propagate new values - for head in self.chart[rule.head_fn].intern.itervalues(): - self.agenda[head] = self.time_step - self.time_step += 1 - - return self.go() - - def go(self): - try: - return self._go() - except KeyboardInterrupt: - print '^C' - self.dump_charts() + item.aggregator.inc(val, ruleix, variables) + self.agenda[item] = self.time_step + self.time_step += 1 - def _go(self, changed=None): + def run_agenda(self, changed=None): "the main loop" if changed is None: changed = {} @@ -363,7 +189,7 @@ class Interpreter(object): self.run_uninitialized() if self.agenda: - self._go(changed) + self.run_agenda(changed) return changed @@ -403,12 +229,6 @@ class Interpreter(object): # this is not possible. self.emit(*e) - def new_updater(self, fn, ruleix, handler): - self.updaters[fn].append(handler) - rule = self.rules[ruleix] - rule.updaters.append(handler) - handler.rule = rule - def gbc(self, fn, *args): # TODO: need to distinguish `unknown` from `null` @@ -432,32 +252,7 @@ class Interpreter(object): return head.value - def new_query(self, fn, ruleix, handler): - self._gbc[fn].append(handler) - rule = self.rules[ruleix] - assert rule.query is None - rule.query = handler - handler.rule = rule - rule.head_fn = fn - - 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) - - def emit(self, item, val, ruleix, variables, delete): - if delete: - item.aggregator.dec(val, ruleix, variables) - else: - item.aggregator.inc(val, ruleix, variables) - self.agenda[item] = self.time_step - self.time_step += 1 - - def do(self, filename): + def load_plan(self, filename): """ Compile, load, and execute new dyna rules. @@ -469,10 +264,11 @@ class Interpreter(object): env = imp.load_source('dynamically_loaded_module', filename) + anf = {} if path(filename + '.anf').exists(): # XXX: should have codegen provide this in plan.py with file(filename + '.anf') as f: - for anf in read_anf(f.read()): - self.rules[anf.ruleix].anf = anf + for x in read_anf(f.read()): + anf[x.ruleix] = x for k,v in [('chart', self.chart), ('build', self.build), @@ -484,100 +280,304 @@ class Interpreter(object): self.new_fn(k, v) new_rules = set() - for fn, r, h in env.queries: - self.new_query(fn, r, h) - new_rules.add(r) + for fn, index, h in env.queries: + new_rules.add(index) + self.add_rule(index, query=h, head_fn=fn, anf=anf[index]) + + for index, h in env.initializers: + new_rules.add(index) + self.add_rule(index, init=h, anf=anf[index]) + for fn, r, h in env.updaters: self.new_updater(fn, r, h) - for r, h in env.initializers: - self.new_initializer(r, h) - new_rules.add(r) - self.uninitialized_rules.append((None, r)) - self.new_rules = new_rules # accept the new parser state self.parser_state = env.parser_state - # ------ $rule for fun and profit ------- - interp = self - def rule(ix, *a): - fn = '$rule/%s' % (len(a) + 1) - if interp.agg_name[fn] is None: - interp.new_fn(fn, ':=') - item = interp.build(fn, ix, *a) - interp.emit(item, true, ruleix=None, variables=None, delete=False) - return item - for i in new_rules: - r = self.rules[i] - if hasattr(r, 'anf'): # XXX: all rules should have ANF! - agg, head, evals, unifs, result = r.anf[2:] - r.item = rule(i, r.src, todyna([head, agg, result, evals, unifs]), r.init, r.query) - #----------------------------------------- - - self.run_uninitialized() - - return self.go() + return new_rules def run_uninitialized(self): - q = list(self.uninitialized_rules) failed = [] - - self.uninitialized_rules = [] - while q: - (_, r) = q.pop() + rule = q.pop() try: - - rule = self.rules[r] assert not rule.initialized - emits = [] def _emit(*args): emits.append(args) + # clear error, if any + if rule in self.error: + del self.error[rule] + rule.init(emit=_emit) except (ZeroDivisionError, TypeError, KeyboardInterrupt, RuntimeError, OverflowError) as e: + # TODO: should put stuff in error table, like everything else. e.exception_frame = rule_error_context() e.traceback = traceback.format_exc() - failed.append((e, r)) + self.error[rule] = e + + failed.append(rule) else: rule.initialized = True # process emits for e in emits: self.emit(*e, delete=False) - self.uninitialized_rules = failed + #___________________________________________________________________________ + # Adding/removing rules + + def add_rule(self, index, init=None, query=None, head_fn=None, anf=None): + + assert index not in self.rules + + span = hide_ugly_filename(parse_attrs(init or query)['Span']) + dyna_src = strip_comments(parse_attrs(init or query)['rule']) + + rule = self.rules[index] = Rule(index) + + rule.span = span + rule.src = dyna_src + rule.anf = anf + rule.head_fn = head_fn + + if init: + rule.init = init + init.rule = rule + self.uninitialized_rules.append(rule) + + elif query: + self._gbc[head_fn].append(query) + rule.query = query + query.rule = rule + + else: + assert False, "Can't add rule with out an initializer or query handler." + + # XXX: all rules should eventually have ANF tacked on, but until then... + if anf is not None: + agg, head, evals, unifs, result = anf[2:] + args = (index, + dyna_src, + todyna([head, agg, result, evals, unifs]), + init, + query) + + fn = '$rule/%s' % (len(args) + 1) + if self.agg_name[fn] is None: + self.new_fn(fn, ':=') + + rule.item = self.build(fn, *args) + self.emit(rule.item, true, ruleix=None, variables=None, delete=False) + + def retract_rule(self, idx): + "Retract rule and all of it's edges." + + try: + rule = self.rules.pop(idx) + except KeyError: + print 'Rule %s not found.' % idx + return + + # remove $rule + if hasattr(rule, 'item'): + self.delete_emit(rule.item, true, ruleix=None, variables=None) + + if rule.init is not None: + # Forward chained rule -- + # remove update handlers + for u in rule.updaters: + for xs in self.updaters.values(): + if u in xs: + xs.remove(u) + assert u not in xs, 'Several occurrences of u in xs' + if rule.initialized: + # run initializer in delete mode + try: + rule.init(emit=self.delete_emit) + except (ZeroDivisionError, TypeError, KeyboardInterrupt, RuntimeError, OverflowError): + # TODO: what happens if there's an error? + pass + else: + self.uninitialized_rules.remove(rule) + + else: + # Backchained rule -- + # remove query handler + self._gbc[rule.head_fn].remove(rule.query) + # blast the memo entries for items this rule may have helped derive. + if rule.head_fn in self.chart: + + # update values before propagating + for head in self.chart[rule.head_fn].intern.itervalues(): + def _emit(item, val, ruleix, variables): + item.aggregator.dec(val, ruleix, variables) + try: + rule.query(*head.args, emit=_emit) + except (ZeroDivisionError, TypeError, KeyboardInterrupt, RuntimeError, OverflowError): + # TODO: what happens if there's an error? + pass + + # propagate new values + for head in self.chart[rule.head_fn].intern.itervalues(): + self.agenda[head] = self.time_step + self.time_step += 1 + + return self.run_agenda() + + def new_updater(self, fn, ruleix, handler): + self.updaters[fn].append(handler) + rule = self.rules[ruleix] + rule.updaters.append(handler) + handler.rule = rule + + #___________________________________________________________________________ + # Communication with Dyna compiler + def dynac(self, filename): + """ + Compile a file full of dyna code. Note: this routine does not pass along + parser_state. + """ filename = path(filename) self.files.append(filename) out = self.tmp / filename.read_hexhash('sha1') + '.plan.py' -# out = filename + '.plan.py' + #out = filename + '.plan.py' self.files.append(out) dynac(filename, out) return out def dynac_code(self, code): - """ - Compile a string of dyna code. - - raises ``DynaCompilerError`` - """ + "Compile a string of dyna code." x = sha1() x.update(self.parser_state) x.update(code) - dyna = self.tmp / ('%s.dyna' % x.hexdigest()) - with file(dyna, 'wb') as f: f.write(self.parser_state) # include parser state if any. f.write(code) - return self.dynac(dyna) + #___________________________________________________________________________ + # Routines for showing things to the user. + + def dump_charts(self, out=None): + if out is None: + out = sys.stdout + fns = self.chart.keys() + fns.sort() + fns = [x for x in fns if x not in self._gbc] # don't show backchained items + nullary = [x for x in fns if x.endswith('/0')] + others = [x for x in fns if not x.endswith('/0')] + + # show nullary charts first + nullary = filter(None, [str(self.chart[x]) for x in nullary]) + charts = filter(None, [str(self.chart[x]) for x in others if not x.startswith('$rule/')]) + + if nullary or charts: + print >> out + print >> out, 'Solution' + print >> out, '========' + else: + print >> out, 'Solution empty.' + + if nullary: + for line in nullary: + print >> out, line + print >> out + for line in charts: + print >> out, line + print >> out + + self.dump_errors(out) + + def dump_errors(self, out=None): + if out is None: + out = sys.stdout + # We only dump the error chart if it's non empty. + if not self.error and not self.uninitialized_rules: + return + print >> out + print >> out, red % 'Errors' + print >> out, red % '======' + + # separate errors into aggregation errors and update handler errors + I = defaultdict(lambda: defaultdict(list)) + E = defaultdict(lambda: defaultdict(list)) + for item, x in self.error.items(): + if isinstance(item, Rule): + continue + (val, es) = x + for e, h in es: + if h is None: + I[item.fn][type(e)].append((item, val, e)) + else: + E[h.rule][type(e)].append((item, val, e)) + + # aggregation errors + for r in sorted(I, key=lambda r: r.index): + print >> out, 'Error(s) aggregating %s:' % r + for etype in I[r]: + print >> out, ' %s:' % etype.__name__ + for i, (item, value, e) in enumerate(sorted(I[r][etype])): + if i >= 5: + print >> out, ' %s more ...' % (len(I[r][etype]) - i) + break + print >> out, ' `%s`: %s' % (item, e) + print >> out + + # errors pertaining to rules + for r in sorted(E, key=lambda r: r.index): + print >> out, 'Error(s) in rule:', r.span + print >> out + for line in r.src.split('\n'): + print >> out, ' ', line + print >> out + for etype in E[r]: + print >> out, ' %s:' % etype.__name__ + for i, (item, value, e) in enumerate(sorted(E[r][etype])): + if i >= 5: + print >> out, ' %s more ...' % (len(E[r][etype]) - i) + break + print >> out, ' when `%s` = %s' % (item, _repr(value)) + print >> out, ' %s' % (e) + print >> out + print >> out, r.render_ctx(e.exception_frame, indent=' ') + print >> out + + # uninitialized rules + if self.uninitialized_rules: + print >> out, red % 'Uninitialized rules' + print >> out, red % '===================' + for rule in self.uninitialized_rules: + e = self.error[rule] + print >> out, 'Failed to initialize rule:' + print >> out, ' ', rule.src + print >> out, ' due to `%s`' % e + print >> out, rule.render_ctx(e.exception_frame, indent=' ') + print >> out + + print >> out + + def dump_rules(self): + if not self.rules: + print 'No rules found.' + return + print + print 'Rules' + print '=====' + for i in sorted(self.rules): + rule = self.rules[i] + if rule.init is not None and not rule.initialized: + print '%3s: %s <-- uninitialized' % (i, rule.src) + else: + print '%3s: %s' % (i, rule.src) + print + def peel(fn, item): """ @@ -585,6 +585,5 @@ def peel(fn, item): functor/arity, `fn`. Returns the arguments of term as a tuple of intern idxs and constants (possibly an empty tuple). """ - assert isinstance(item, Term) - assert item.fn == fn + assert isinstance(item, Term) and item.fn == fn return item.args diff --git a/src/Dyna/Backend/Python/load/__init__.py b/src/Dyna/Backend/Python/load/__init__.py index c5f3d0d..a8ab571 100644 --- a/src/Dyna/Backend/Python/load/__init__.py +++ b/src/Dyna/Backend/Python/load/__init__.py @@ -18,4 +18,4 @@ def run(interp, line): m = get_module('load', module)(interp, name) exec 'm.main(%s)' % args - return interp.go() + return interp.run_agenda() diff --git a/src/Dyna/Backend/Python/main.py b/src/Dyna/Backend/Python/main.py index 36abf05..37a80b0 100644 --- a/src/Dyna/Backend/Python/main.py +++ b/src/Dyna/Backend/Python/main.py @@ -10,11 +10,9 @@ import post, load def main(): parser = argparse.ArgumentParser(description="The dyna interpreter!") parser.add_argument('source', nargs='*', type=path, - help='Path to Dyna source file (or plan if --plan=true).') + help='Path to Dyna source file.') parser.add_argument('-i', dest='interactive', action='store_true', help='Fire-up REPL after runing solver..') - parser.add_argument('--plan', action='store_true', - help='`source` specifies output of the compiler instead of dyna source code.') parser.add_argument('-o', '--output', dest='output', type=argparse.FileType('wb'), help='Write solution to file.') @@ -54,35 +52,14 @@ def main(): print 'File `%s` does not exist.' % args.source return - if args.plan: - # copy plan to tmp directory - plan = interp.tmp / args.source.read_hexhash('sha1') + '.plan.py' - args.source.copy(plan) + try: + plan = interp.dynac(args.source) + except DynaCompilerError as e: + print e + exit(1) - else: - try: - plan = interp.dynac(args.source) - except DynaCompilerError as e: - print e - exit(1) - -# if args.profile: -# # When profiling, its common practice to disable the garbage collector. -# import gc -# gc.disable() -# -# from cProfile import Profile -# p = Profile() -# p.runctx('interp.do(plan)', globals(), locals()) -# p.dump_stats('prof') -# -# interp.dump_charts() -# -# os.system('gprof2dot.py -f pstats prof | dot -Tsvg -o prof.svg && eog prof.svg &') -# os.system('pkill snakeviz; snakeviz prof &') -# return - - interp.do(plan) + interp.load_plan(plan) + interp.run_agenda() if args.load: for cmd in args.load: diff --git a/src/Dyna/Backend/Python/repl.py b/src/Dyna/Backend/Python/repl.py index 6757c44..c965d1e 100644 --- a/src/Dyna/Backend/Python/repl.py +++ b/src/Dyna/Backend/Python/repl.py @@ -164,38 +164,29 @@ class REPL(cmd.Cmd, object): print "Queries don't end with a dot." return - self.interp.new_rules = set() + query = "$query dict= %s." % q - try: - query = "$query dict= %s." % q - - self.default(query, show_changed=False) - - try: - [(_, _, results)] = self.interp.chart['$query/0'][:,] + (new_rules, _changed) = self.default(query, show_changed=False) - return [dict(r) for r in topython(results)] + try: + [(_, _, results)] = self.interp.chart['$query/0'][:,] + return [dict(r) for r in topython(results)] - except ValueError: - return [] + except ValueError: + return [] finally: - - # cleanup: - # retract newly added rules. - for r in self.interp.new_rules: - if r in self.interp.rules: - self.interp.retract_rule(r) + # cleanup: retract temporary rules used to answer query. + for r in new_rules: + self.interp.retract_rule(r) try: - # drop $out chart + # drop temporary chart del self.interp.chart['$query/0'] except KeyError: # query must have failed. pass - self.interp.new_rules = set() - def do_vquery(self, q): """ See query. @@ -261,17 +252,20 @@ class REPL(cmd.Cmd, object): try: src = self.interp.dynac_code(line + ' %% repl line %s' % self.lineno) - changed = self.interp.do(src) - except DynaCompilerError as e: print type(e).__name__ + ':' print e print 'new rule(s) were not added to program.' print else: + new_rules = self.interp.load_plan(src) + changed = self.interp.run_agenda() + if show_changed: self._changed(changed) + return (new_rules, changed) + def _changed(self, changed): if not changed: return @@ -299,59 +293,6 @@ class REPL(cmd.Cmd, object): finally: readline.write_history_file(self.hist) -# def do_subscribe(self, line): -# """ -# Establish a subscription to the results of a query. -# -# For example, -# -# > subscribe f(X,X) -# > f(1,1) := 1. f(1,2) := 2. f(2,2) := 3. -# Changes -# ======= -# f(X,X): -# 1 where {X=1} -# -# To view all subscriptions: -# -# > subscriptions -# f(X): -# 1 where {X=1} -# 2 where {X=2} -# -# """ -# if line.endswith('.'): -# print "Queries don't end with a dot." -# return -# # subscriptions are maintained via forward chaining. -# query = '$subscribed(%s, %s) dict= %s.' % (self.lineno, _repr(line), line) -# self.default(query) -# -# def do_subscriptions(self, _): -# "List subscriptions. See subscribe." -# for (_, [_, q], results) in self.interp.chart['$subscribed/2'][:,:,:]: -# if results: -# print q -# for result in results: -# print ' ', _repr(result.value), 'where', drepr(dict(result.variables)) -# print - -# def _changed_subscriptions(self, changed): -# -# # TODO: this doesn't show changes - it redumps everything. -# -# if not changed: -# return -# for x, _ in sorted(changed.items()): -# if x.fn == '$subscribed/2': -# [i, q] = x.args -# if x.value: -# print '%s: %s' % (i, q) -# for result in x.value: -# print ' ', _repr(result.value), 'where', drepr(dict(result.variables)) -# print -# self.interp.dump_errors() - def do_help(self, line): mod = line.split() if len(mod) <= 1: @@ -562,45 +503,35 @@ class REPL(cmd.Cmd, object): def _trace(self, q, depth_limit=-1): - self.interp.new_rules = set() + query = "$trace dict= _ is (%s), &(%s)." % (q,q) - try: - query = "$trace dict= _ is (%s), &(%s)." % (q,q) - - self.default(query, show_changed=False) - - try: - [(_, _, results)] = self.interp.chart['$trace/0'][:,] - - results = topython(results) - results = [dict(r)['$val'] for r in results] - - except ValueError: - print 'no items matching `%s`.' % q - return - - from post.trace import Tracer - tracer = Tracer(self.interp) - - for item in results: - print - tracer(todyna(item), depth_limit=depth_limit) + (new_rules, _changed) = self.default(query, show_changed=False) + try: + [(_, _, results)] = self.interp.chart['$trace/0'][:,] + results = topython(results) + results = [dict(r)['$val'] for r in results] + except ValueError: + print 'no items matching `%s`.' % q + return finally: - # cleanup: - # retract newly added rules. - for r in self.interp.new_rules: - if r in self.interp.rules: - self.interp.retract_rule(r) + # cleanup: retract temporary rules used to answer query. + for r in new_rules: + self.interp.retract_rule(r) - try: - # drop $out chart - del self.interp.chart['$trace/0'] - except KeyError: - # query must have failed. - pass + from post.trace import Tracer + tracer = Tracer(self.interp) - self.interp.new_rules = set() + for item in results: + print + tracer(todyna(item), depth_limit=depth_limit) + + try: + # drop temporary chart + del self.interp.chart['$trace/0'] + except KeyError: + # query must have failed. + pass do_load.__doc__ = do_load.__doc__.format(load=', '.join(load.available)) diff --git a/src/Dyna/Main/TestsDriver.hs b/src/Dyna/Main/TestsDriver.hs index 9c3b7ea..73a5317 100644 --- a/src/Dyna/Main/TestsDriver.hs +++ b/src/Dyna/Main/TestsDriver.hs @@ -4,7 +4,7 @@ module Dyna.Main.TestsDriver where import Test.Framework import qualified Dyna.Analysis.Mode.Selftest as DAMS -- import qualified Dyna.Backend.K3.Selftest as DBK3S -import qualified Dyna.Backend.Python.Selftest as DBPS +--import qualified Dyna.Backend.Python.Selftest as DBPS import qualified Dyna.ParserHS.Selftest as DPHS import qualified Dyna.XXX.TrifectaTests as DXT @@ -17,5 +17,5 @@ main = defaultMain -- XXX Until this is meaningful... -- ,DBK3S.selftest - , DBPS.selftest + --, DBPS.selftest ] diff --git a/test/repl/retract-bc-2.dynadoc b/test/repl/retract-bc-2.dynadoc new file mode 100644 index 0000000..b5a9d5e --- /dev/null +++ b/test/repl/retract-bc-2.dynadoc @@ -0,0 +1,96 @@ +> :- backchain h/1. +| :- backchain g/1. +| :- backchain f/1. +| h(X) += g(X)/2. +| h(X) += f(X)/2. +| a(X) = h(X) for X in range(6). +| +| g(X) = X*f(X-1) for X > 0. +| f(X) = X*g(X-1) for X > 0. +| f(0) = 1. +| g(0) = 1. +| c(X) = (f(X) + g(X)) / 2 for X in range(1,6). % skip the base case. + +Changes +======= +a(0) = 1.0. +a(1) = 1.0. +a(2) = 2.0. +a(3) = 6.0. +a(4) = 24.0. +a(5) = 120.0. +c(1) = 1.0. +c(2) = 2.0. +c(3) = 6.0. +c(4) = 24.0. +c(5) = 120.0. + +> rules + +Rules +===== + 0: h(X) += g(X)/2. + 1: h(X) += f(X)/2. + 2: a(X) = h(X) for X in range(6). + 3: g(X) = X*f(X-1) for X > 0. + 4: f(X) = X*g(X-1) for X > 0. + 5: f(0) = 1. + 6: g(0) = 1. + 7: c(X) = (f(X) + g(X)) / 2 for X in range(1,6). + +> retract_rule 3 + +Changes +======= +c(1) = null. +c(2) = null. +c(3) = null. +c(4) = null. +c(5) = null. +g(1) = null. +g(2) = null. +g(3) = null. +g(4) = null. +g(5) = null. + +% as in `retract-bc.dynadoc`, `a(X)` did not change even tho it ought to. The +% `c(X)` values get blasted because the don't have an oblivious memoized item +% inbetween like `h(X)`. + +> b(X) = h(X) for X in range(6). + +Changes +======= +b(0) = 1.0. +b(1) = 1.0. +b(2) = 2.0. +b(3) = 6.0. +b(4) = 24.0. +b(5) = 120.0. + +% Just to hammer the point home, the rule above shows that `h(X)` values are +% still memoized! It doesn't know that one of it's antecedents has changed. + +> sol + +Solution +======== +a/1 +=== +a(0) = 1.0. +a(1) = 1.0. +a(2) = 2.0. +a(3) = 6.0. +a(4) = 24.0. +a(5) = 120.0. + +b/1 +=== +b(0) = 1.0. +b(1) = 1.0. +b(2) = 2.0. +b(3) = 6.0. +b(4) = 24.0. +b(5) = 120.0. + +% TODO: redefine `g` see if the other rules pick up the new definition. -- 2.50.1