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)
self.parser_state = ''
self.files = []
# rules
- self.rules = ddict(Rule)
+ self.rules = {}
self.updaters = defaultdict(list)
self._gbc = defaultdict(list)
# data structures
# 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':
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 = {}
self.run_uninitialized()
if self.agenda:
- self._go(changed)
+ self.run_agenda(changed)
return changed
# 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`
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.
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),
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):
"""
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
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.
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
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:
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))