]> hydra-www.ietfng.org Git - dyna2/commitdiff
added test/repl
authortimv <tim.f.vieira@gmail.com>
Wed, 12 Jun 2013 01:46:57 +0000 (21:46 -0400)
committertimv <tim.f.vieira@gmail.com>
Wed, 12 Jun 2013 01:46:57 +0000 (21:46 -0400)
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).

src/Dyna/Backend/Python/Backend.hs
src/Dyna/Backend/Python/chart.py
src/Dyna/Backend/Python/graph.py
src/Dyna/Backend/Python/interpreter.py
src/Dyna/Backend/Python/repl.py
src/Dyna/Backend/Python/term.py [new file with mode: 0644]
src/Dyna/Backend/Python/utils.py
test/repl/aggregator-conflict [new file with mode: 0755]
test/repl/aggregator-conflict.expect [new file with mode: 0644]
test/repl/retract-rule [new file with mode: 0755]
test/repl/retract-rule.expect [new file with mode: 0644]

index 85d599a5329e9f3322780ed0b7549244fdc3ada1..a6e457c4a88e2c65057c505e08f976ec3410920e 100644 (file)
@@ -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
index a9a8b0cec2a2de7772d3e936716b049c7eb003b8..b2fb50cd574571554c5e8560076c091f83f68f6f 100644 (file)
@@ -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):
index aa0f21353476aad021614126c9338d32ae1ec4f8..edd64ae8898b25018f2639e627c9cd294bfd77e2 100644 (file)
@@ -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'
index 70c918572f14136fc8efea638bb29833d02ff6da..38247ba489b80f4ce4192506c59134e1211be92b 100644 (file)
@@ -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__':
index 89ed971cf4238671b08056b931334b8970953e93..a3ea9b76279531a02aff8133cb58b6da40664a7c 100644 (file)
@@ -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 (file)
index 0000000..dd88c0c
--- /dev/null
@@ -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
index fed7af0a80fa3595290479815748cc83e71d1cbc..4a35be758a2f59eec3e76740d03076a546405052 100644 (file)
@@ -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 (executable)
index 0000000..3fd4ee5
--- /dev/null
@@ -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 (file)
index 0000000..3238426
--- /dev/null
@@ -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 (executable)
index 0000000..e96b1c2
--- /dev/null
@@ -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 (file)
index 0000000..1552807
--- /dev/null
@@ -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