]> hydra-www.ietfng.org Git - dyna2/commitdiff
Dropped end-to-end tests from haskell test runner (removed support for --plan
authorTim Vieira <tim.f.vieira@gmail.com>
Tue, 16 Jul 2013 18:26:25 +0000 (14:26 -0400)
committerTim Vieira <tim.f.vieira@gmail.com>
Tue, 16 Jul 2013 18:26:25 +0000 (14:26 -0400)
flag for ./dyna). These test are now part of python test runnner.

Minor refactoring/reorganization of interpreter and repl.

examples/dijkstra-backpointers.dyna
run-doctests.py
src/Dyna/Backend/Python/Selftest.hs [deleted file]
src/Dyna/Backend/Python/interpreter.py
src/Dyna/Backend/Python/load/__init__.py
src/Dyna/Backend/Python/main.py
src/Dyna/Backend/Python/repl.py
src/Dyna/Main/TestsDriver.hs
test/repl/retract-bc-2.dynadoc [new file with mode: 0644]

index 68f5f97e93a86be2923fc090508fb9b5d637e48f..68b8fec194d3005f8c98fa2fe7ddbe495def5b62 100644 (file)
@@ -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.
 
index cb4f732dc89924cc4a4f92c2d4267b37cd93099e..32ecb741525850ea606125c828cfcbbd8a589014 100755 (executable)
@@ -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 (file)
index 19385e8..0000000
+++ /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"]
-
-------------------------------------------------------------------------}}}
index 2bdafca786691e03ba7bb6805d52bdc65af8c284..587e4ee5400da12045ff80cd86c2427c61bb7063 100644 (file)
@@ -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
index c5f3d0d3a29230aa8b3f02bce28434176561cc58..a8ab57151830666a5d2805bbe7f26c280ea7990e 100644 (file)
@@ -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()
index 36abf05094241e385231ba1693bd66876ee317a4..37a80b087560712cbcbcb1e1514249b82230127a 100644 (file)
@@ -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:
index 6757c44ffc831d3d046c65e4aae4478b07780dfc..c965d1e3bc9e6ce57044aaa7426a028ac97eb81e 100644 (file)
@@ -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))
index 9c3b7eaa43721552e4a25e2c84215d2872749801..73a5317cc53354d8193fd60d279f79731d26f4bf 100644 (file)
@@ -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 (file)
index 0000000..b5a9d5e
--- /dev/null
@@ -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.