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).
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 ()
`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
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):
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'
- 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.
- TODO: BAggregators aren't very efficient.
+ - interning with integers instead of deduplicated instances of Term.
+
STRONGER (robustness)
=====================
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
=====
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)
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]))
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
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:
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()
# 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)
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:
# 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
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__':
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
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)
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.'
--- /dev/null
+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
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)
--- /dev/null
+#!/usr/bin/env bash
+
+echo -e "
+a += 1.
+a." |./dyna > $0.out
+
+colordiff $0.expect $0.out && echo pass
--- /dev/null
+:- :- =============
+a := 1
+
+:- AggregatorConflict: Aggregator conflict 'a/0' was '+=' trying to set to '|='.
+> new rule(s) were not added to program.
+:- exit
--- /dev/null
+#!/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
--- /dev/null
+:- :- =============
+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