samovar-py 0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
samovar/__init__.py ADDED
File without changes
samovar/ast.py ADDED
@@ -0,0 +1,56 @@
1
+ # encoding: UTF-8
2
+
3
+ from collections import namedtuple
4
+
5
+
6
+ # Python 2/3
7
+ try:
8
+ unicode = unicode
9
+ except NameError:
10
+ unicode = str
11
+
12
+
13
+ World = namedtuple('World', ['scenarios'])
14
+
15
+
16
+ class Rule(namedtuple('Rule', ['pre', 'words', 'post'])):
17
+ __slots__ = ()
18
+
19
+ def format(self, unifier):
20
+ return join_sentence_parts([unicode(t.subst(unifier)) for t in self.words])
21
+
22
+ def to_json(self):
23
+ return join_sentence_parts([unicode(t) for t in self.words])
24
+
25
+
26
+ Scenario = namedtuple('Scenario', ['name', 'propositions', 'rules', 'goal'])
27
+ Cond = namedtuple('Cond', ['exprs', 'bindings'])
28
+ Assert = namedtuple('Assert', ['term'])
29
+ Retract = namedtuple('Retract', ['term'])
30
+
31
+
32
+ def join_sentence_parts(parts):
33
+ acc = u''
34
+ quote_open = False
35
+ for part in parts:
36
+ last = '' if not acc else acc[-1]
37
+ if last == '':
38
+ acc += part
39
+ elif last == '"' and quote_open:
40
+ acc += part
41
+ elif last == '"' and not quote_open:
42
+ if part in (u'.', u',', u'!', u'?'):
43
+ acc += part
44
+ else:
45
+ acc += ' ' + part
46
+ elif last == ',' and part == u'"' and not quote_open:
47
+ acc += ' ' + part
48
+ elif last in (u'.', u',', u'!', u'?', u"'") and part == u'"' and not quote_open:
49
+ acc += part
50
+ elif part in (u'.', u',', u'!', u'?', u"'", u'"'):
51
+ acc += part
52
+ else:
53
+ acc += u' ' + part
54
+ if part == '"':
55
+ quote_open = not quote_open
56
+ return acc
samovar/database.py ADDED
@@ -0,0 +1,74 @@
1
+ from samovar.ast import Assert, Retract
2
+
3
+
4
+ class Database(object):
5
+ """A database is a set of things true at some point in time in a world."""
6
+
7
+ def __init__(self, propositions, sorted_search=True):
8
+ """Create a new Database object with the given propositions.
9
+
10
+ While the construction and iteration of the underlying set in Python 2 seems to be
11
+ stable, when running under Python 3, we get an unpredictable order when iterating
12
+ over the set. The `sorted_search` option controls whether we sort the iterable
13
+ before making matches, so that we can accumulate them in a predictable order.
14
+ This is valuable for tests, but can be turned off to improve performance.
15
+
16
+ """
17
+ self.sorted_search = sorted_search
18
+ self.contents = set()
19
+ for term in propositions:
20
+ self.contents.add(term)
21
+
22
+ def clone(self):
23
+ return Database(self.contents, sorted_search=self.sorted_search)
24
+
25
+ def add(self, term):
26
+ self.contents.add(term)
27
+
28
+ def remove(self, term):
29
+ self.contents.remove(term)
30
+
31
+ def match_all(self, patterns, env):
32
+ """Find all matches for the given patterns in the database, and return a list of unifiers.
33
+
34
+ `database` is an iterable of `Term`s.
35
+ `patterns` is a list of `Assert`s and `Retract`s.
36
+ `env` is a dict mapping `Var` names to `Term`s (a previous unifier.)
37
+
38
+ """
39
+ if not patterns:
40
+ return [env]
41
+ envs = []
42
+ pattern = patterns[0]
43
+
44
+ if isinstance(pattern, Assert):
45
+ for proposition in (sorted(self.contents) if self.sorted_search else self.contents):
46
+ unifier = pattern.term.match(proposition, env, unique_binding=True)
47
+ if unifier is None:
48
+ continue
49
+ envs.extend(self.match_all(patterns[1:], unifier))
50
+
51
+ elif isinstance(pattern, Retract):
52
+ expanded_pattern = pattern.term.subst(env)
53
+
54
+ # To test a negative match, we require that there are
55
+ # no free variables remaining in our pattern. If there are,
56
+ # it will simply not match. TODO add a flag to turn this
57
+ # check on to support better debugging.
58
+
59
+ # free_vars = set()
60
+ # expanded_pattern.collect_variables(free_vars)
61
+ # if free_vars:
62
+ # # TODO: better exception than this
63
+ # raise NotImplementedError
64
+
65
+ # now we simply check if the term exists in the database.
66
+ # if it does not, we recurse down to the next clause in the pattern.
67
+
68
+ if not (expanded_pattern in self.contents):
69
+ envs.extend(self.match_all(patterns[1:], env))
70
+
71
+ else:
72
+ raise NotImplementedError
73
+
74
+ return envs
File without changes
@@ -0,0 +1,64 @@
1
+ # encoding: UTF-8
2
+
3
+ import sys
4
+
5
+ from samovar.ast import Assert, Retract
6
+
7
+
8
+ # Python 2/3
9
+ try:
10
+ xrange = xrange
11
+ except NameError:
12
+ xrange = range
13
+
14
+ # Python 2/3
15
+ try:
16
+ unicode = unicode
17
+ except NameError:
18
+ unicode = str
19
+
20
+
21
+ class Event(object):
22
+ def __init__(self, rule, unifier):
23
+ self.rule = rule
24
+ self.unifier = unifier
25
+
26
+ def to_json(self):
27
+ u = dict([(unicode(k), unicode(v)) for k, v in self.unifier.items()])
28
+ return [self.rule.to_json(), u]
29
+
30
+ def __str__(self):
31
+ return self.rule.format(self.unifier)
32
+
33
+
34
+ class BaseGenerator(object):
35
+ def __init__(self, world, scenario, **kwargs):
36
+ self.world = world
37
+ self.scenario = scenario
38
+
39
+ def goal_is_met(self, state):
40
+ matches = state.match_all(self.scenario.goal.exprs, self.scenario.goal.bindings)
41
+ return len(matches) > 0
42
+
43
+ def candidate_rules(self, state, require_change=False):
44
+ """A generator."""
45
+ for rule in self.scenario.rules:
46
+ for unifier in state.match_all(rule.pre.exprs, rule.pre.bindings):
47
+ if require_change and not rule.post.exprs:
48
+ # Rules that don't change the state of the world are not worth considering.
49
+ continue
50
+ yield (rule, unifier)
51
+
52
+ def update_state(self, state, env, rule):
53
+ for expr in rule.post.exprs:
54
+ term = expr.term.subst(env)
55
+ if isinstance(expr, Assert):
56
+ state.add(term)
57
+ elif isinstance(expr, Retract):
58
+ state.remove(term)
59
+
60
+ def debug_state(self, state, label):
61
+ sys.stderr.write(":::: {} State [\n".format(label))
62
+ for term in sorted(state.contents):
63
+ sys.stderr.write(":::: {}\n".format(term))
64
+ sys.stderr.write(":::: ]\n")
@@ -0,0 +1,57 @@
1
+ # encoding: UTF-8
2
+
3
+ """A breadth-first searching Generator that finds the
4
+ shortest path of events that leads to the goal state
5
+ (if any such path exists).
6
+ """
7
+
8
+ import sys
9
+ import time
10
+
11
+ from samovar.ast import Assert, Retract
12
+ from samovar.database import Database
13
+
14
+ from .base import Event, BaseGenerator
15
+
16
+
17
+ class BreadthFirstGenerator(BaseGenerator):
18
+ def __init__(self, world, scenario, verbosity=0, sorted_search=True, randomness=None):
19
+ self.world = world
20
+ self.scenario = scenario
21
+ self.verbosity = verbosity
22
+ self.sorted_search = sorted_search
23
+ self.random = randomness
24
+
25
+ def generate_events(self, **kwargs):
26
+ situations = [
27
+ ([], Database(self.scenario.propositions, sorted_search=self.sorted_search))
28
+ ]
29
+
30
+ seen_states = set()
31
+
32
+ while True:
33
+ new_situations = []
34
+ if self.verbosity >= 1:
35
+ sys.stderr.write("Considering {} situations\n".format(len(situations)))
36
+ start_time = time.time()
37
+ for (events, state) in situations:
38
+ for rule, unifier in self.candidate_rules(state, require_change=True):
39
+ new_event = Event(rule, unifier)
40
+ new_state = state.clone()
41
+ self.update_state(new_state, unifier, rule)
42
+ froz = frozenset(new_state.contents)
43
+ if froz in seen_states:
44
+ continue
45
+ seen_states.add(froz)
46
+ new_events = events + [new_event]
47
+ if self.goal_is_met(new_state):
48
+ return new_events
49
+ new_situations.append(
50
+ (new_events, new_state)
51
+ )
52
+ if self.verbosity >= 1:
53
+ end_time = time.time()
54
+ duration = end_time - start_time
55
+ sys.stderr.write("Considered {} situations in {} seconds ({} situations / second)\n".format(len(situations), duration, float(len(situations))/duration))
56
+ sys.stderr.write("Installing {} new situations\n".format(len(new_situations)))
57
+ situations = new_situations
@@ -0,0 +1,73 @@
1
+ # encoding: UTF-8
2
+
3
+ # FIXME: just a sketch for now
4
+
5
+ import sys
6
+ import time
7
+
8
+ from samovar.ast import Assert, Retract
9
+ from samovar.database import Database
10
+
11
+ from .base import Event, BaseGenerator
12
+
13
+
14
+ class DepthFirstGenerator(BaseGenerator):
15
+ def __init__(self, world, scenario, verbosity=0, sorted_search=True, randomness=None):
16
+ self.world = world
17
+ self.scenario = scenario
18
+ self.verbosity = verbosity
19
+ self.sorted_search = sorted_search
20
+ self.random = randomness
21
+ self.seen_states = set()
22
+
23
+ def generate_events(self, **kwargs):
24
+ self.seen_states = set()
25
+
26
+ # Create initial stacks.
27
+ state = Database(self.scenario.propositions, sorted_search=self.sorted_search)
28
+ candidate_rules = list(self.candidate_rules(state, require_change=True))
29
+ stack = [
30
+ [None, state, candidate_rules, 0]
31
+ ]
32
+
33
+ # Loop, doing work at the top of the stack.
34
+ done = False
35
+ counter = 0
36
+ while not done:
37
+ counter += 1
38
+ [last_event, state, candidate_rules, cr_index] = stack[-1]
39
+ if self.verbosity >= 1 and counter % 1000 == 0:
40
+ sys.stderr.write("Stack depth {}, candidate count {}, index {}\n".format(
41
+ len(stack), len(candidate_rules), cr_index
42
+ ))
43
+ if cr_index > (len(candidate_rules) - 1):
44
+ # we've exhausted all the candidates on this level. backtrack.
45
+ if self.verbosity >= 1:
46
+ sys.stderr.write("*** Backtracking\n")
47
+ stack.pop()
48
+ continue
49
+
50
+ # otherwise, try to make a match.
51
+ rule, unifier = candidate_rules[cr_index]
52
+
53
+ new_event = Event(rule, unifier)
54
+ new_state = state.clone()
55
+ self.update_state(new_state, unifier, rule)
56
+ if self.verbosity >= 2:
57
+ self.debug_state(new_state, "Intermediate")
58
+ if self.goal_is_met(new_state):
59
+ return [e[0] for e in stack if e[0] is not None] + [new_event]
60
+
61
+ froz = frozenset(new_state.contents)
62
+ if froz in self.seen_states:
63
+ stack[-1][3] += 1 # inc cr_index. TODO this is so ugly
64
+ continue
65
+ self.seen_states.add(froz)
66
+
67
+ # otherwise, descend to try to extend the sequence of events we've got.
68
+ new_candidate_rules = list(self.candidate_rules(new_state, require_change=True))
69
+ stack.append(
70
+ [new_event, new_state, new_candidate_rules, 0]
71
+ )
72
+
73
+ return None
@@ -0,0 +1,62 @@
1
+ # encoding: UTF-8
2
+
3
+ import sys
4
+
5
+ from samovar.database import Database
6
+
7
+ from .base import xrange, Event, BaseGenerator
8
+
9
+
10
+ class StochasticGenerator(BaseGenerator):
11
+ def __init__(self, world, scenario, verbosity=0, sorted_search=True, randomness=None):
12
+ self.world = world
13
+ self.scenario = scenario
14
+ self.verbosity = verbosity
15
+ self.sorted_search = sorted_search
16
+ self.random = randomness
17
+ self.reset_state()
18
+
19
+ def reset_state(self):
20
+ self.state = Database(self.scenario.propositions, sorted_search=self.sorted_search)
21
+
22
+ def generate_events(self, min_count=0, max_count=100, lengthen_factor=1.5):
23
+ acceptable = False
24
+ count = min_count
25
+ while not acceptable:
26
+ if self.verbosity >= 1:
27
+ sys.stderr.write("Generating {} events\n".format(count))
28
+ self.reset_state()
29
+ if self.verbosity >= 2:
30
+ self.debug_state(self.state, "Initial")
31
+ events = []
32
+ for i in xrange(0, count):
33
+ event = self.generate_event()
34
+ if event is None:
35
+ break
36
+ events.append(event)
37
+ if self.verbosity >= 2:
38
+ self.debug_state(self.state, "Final")
39
+ acceptable = self.goal_is_met(self.state)
40
+ if not acceptable:
41
+ count = int(float(count) * lengthen_factor)
42
+ if count > max_count:
43
+ raise ValueError("{}: count exceeds maximum".format(self.scenario.name))
44
+ return events
45
+
46
+ def generate_event(self):
47
+ candidates = list(self.candidate_rules(self.state))
48
+
49
+ if self.verbosity >= 3:
50
+ sys.stderr.write("Candidate rules:\n")
51
+ for rule, unifiers in candidates:
52
+ sys.stderr.write("-> " + rule.to_json() + "\n")
53
+ sys.stderr.write("---> {}\n".format(unifiers))
54
+ sys.stderr.write("")
55
+
56
+ if not candidates:
57
+ return None
58
+ rule, unifier = self.random.choice(candidates)
59
+ self.update_state(self.state, unifier, rule)
60
+ if self.verbosity >= 3:
61
+ self.debug_state(self.state, "Intermediate")
62
+ return Event(rule, unifier)
samovar/main.py ADDED
@@ -0,0 +1,138 @@
1
+ from argparse import ArgumentParser
2
+ import codecs
3
+ import json
4
+ import sys
5
+
6
+ from samovar.parser import Parser
7
+ from samovar.generators.breadthfirst import BreadthFirstGenerator
8
+ from samovar.generators.depthfirst import DepthFirstGenerator
9
+ from samovar.generators.stochastic import StochasticGenerator
10
+ from samovar.randomness import CannedRandomness
11
+
12
+
13
+ def sys_main():
14
+ return main(sys.argv[1:])
15
+
16
+
17
+ def main(args):
18
+ argparser = ArgumentParser()
19
+
20
+ argparser.add_argument(
21
+ 'input_files', nargs='+', metavar='FILENAME', type=str,
22
+ help='Source files containing the scenario descriptions'
23
+ )
24
+ argparser.add_argument(
25
+ "--verbosity", type=int, default=0,
26
+ help="Show some information about the world as generation takes place"
27
+ )
28
+ argparser.add_argument(
29
+ "--verbose", action="store_true",
30
+ help="Show some progress information (alias for --verbosity=1)"
31
+ )
32
+ argparser.add_argument(
33
+ "--debug", action="store_true",
34
+ help="Show state before and after each move (alias for --verbosity=2)"
35
+ )
36
+ argparser.add_argument(
37
+ "--dump-ast", action="store_true",
38
+ help="Just show the AST and stop"
39
+ )
40
+ argparser.add_argument(
41
+ "--generator", choices=('stochastic', 'breadthfirst', 'depthfirst'), default='stochastic',
42
+ help="Specify which generator engine to use"
43
+ )
44
+ argparser.add_argument(
45
+ "--generate-scenarios", type=str, default=None,
46
+ help="If given, generate only these scenarios"
47
+ )
48
+ argparser.add_argument(
49
+ "--lengthen-factor", type=float, default=2.0,
50
+ help="When scenario goal was not met, multiply number of events to generate by this"
51
+ )
52
+ argparser.add_argument(
53
+ "--min-events", type=int, default=1,
54
+ help="Generate at least this many events for each scenario"
55
+ )
56
+ argparser.add_argument(
57
+ "--max-events", type=int, default=1000000,
58
+ help="Conclude that something has gone wrong and abort if more than this many events are generated"
59
+ )
60
+ argparser.add_argument(
61
+ "--output-type", choices=('naive-text', 'events-json', 'scenarios-json',), default='naive-text',
62
+ help="Specify what to output and in what format"
63
+ )
64
+ argparser.add_argument(
65
+ "--deterministic", action="store_true",
66
+ help="Make the output deterministic, at the cost of performance. "
67
+ "Primarily useful for reproducibility during testing."
68
+ )
69
+ argparser.add_argument(
70
+ "--seed", type=int, default=None,
71
+ help="Set random seed of the Python random number generator, when it is in use."
72
+ )
73
+ argparser.add_argument('--version', action='version', version="%(prog)s 0.6")
74
+
75
+ options = argparser.parse_args(args)
76
+
77
+ verbosity = options.verbosity
78
+ if options.verbose:
79
+ verbosity = max(verbosity, 1)
80
+ if options.debug:
81
+ verbosity = max(verbosity, 2)
82
+
83
+ generator_cls = {
84
+ 'breadthfirst': BreadthFirstGenerator,
85
+ 'depthfirst': DepthFirstGenerator,
86
+ 'stochastic': StochasticGenerator,
87
+ }[options.generator]
88
+
89
+ text = ''
90
+ for arg in options.input_files:
91
+ with codecs.open(arg, 'r', encoding='UTF-8') as f:
92
+ text += f.read()
93
+
94
+ p = Parser(text)
95
+ ast = p.world()
96
+ if options.dump_ast:
97
+ print(ast)
98
+ sys.exit(0)
99
+
100
+ if options.deterministic:
101
+ randomness = CannedRandomness()
102
+ else:
103
+ import random
104
+ if options.seed is not None:
105
+ random.seed(options.seed)
106
+ randomness = random
107
+
108
+ event_buckets = []
109
+ for n, scenario in enumerate(ast.scenarios):
110
+ if verbosity >= 1:
111
+ sys.stderr.write("{}. {}\n".format(n, scenario.name))
112
+ if scenario.goal is None:
113
+ continue
114
+ if options.generate_scenarios is not None and scenario.name not in options.generate_scenarios:
115
+ continue
116
+ g = generator_cls(
117
+ ast, scenario,
118
+ verbosity=verbosity, sorted_search=options.deterministic, randomness=randomness
119
+ )
120
+ events = g.generate_events(
121
+ min_count=options.min_events, max_count=options.max_events, lengthen_factor=options.lengthen_factor
122
+ )
123
+ event_buckets.append(events)
124
+
125
+ if options.output_type == 'naive-text':
126
+ for b in event_buckets:
127
+ for e in b:
128
+ sys.stdout.write("%s\n" % e)
129
+ sys.stdout.write("\n")
130
+ elif options.output_type == 'events-json':
131
+ def jsonify_bucket(b):
132
+ return [e.to_json() for e in b]
133
+ jsonified_buckets = [jsonify_bucket(b) for b in event_buckets]
134
+ sys.stdout.write(json.dumps(jsonified_buckets, indent=4, sort_keys=True))
135
+ elif options.output_type == 'scenarios-json':
136
+ raise NotImplementedError("'scenarios-json' output-type not currently implemented")
137
+ else:
138
+ raise NotImplementedError('Not a valid output-type: {}'.format(options.output_type))
samovar/parser.py ADDED
@@ -0,0 +1,166 @@
1
+ # encoding: UTF-8
2
+
3
+ from samovar.ast import World, Scenario, Rule, Cond, Assert, Retract
4
+ from samovar.terms import Term, Var
5
+ from samovar.scanner import Scanner
6
+
7
+
8
+ # World ::= {Scenario}.
9
+ # Scenario ::= "scenario" Atom "{" {Import | Proposition | Rule | Goal} ["." | ","] "}".
10
+ # Import ::= "import" Atom.
11
+ # Goal ::= "goal" Cond.
12
+ # Proposition ::= Term.
13
+ # Rule ::= Cond {Var | Atom | Punct} Cond.
14
+ # Cond ::= "[" Expr {"," Expr} ["where" {Var "=" Term [","]}"]".
15
+ # Expr ::= Term | NotSym Term.
16
+ # Term ::= Var | Atom ["(" Term {AndSym Term} ")"].
17
+ # Var ::= Qmark | Greek.
18
+ # Qmark ::= '?' Atom.
19
+ # Greek ::= <<one of: αβγδεζθικλμνξοπρστυφχψω>>.
20
+ # Atom ::= <<A-Za-z_>> <<A-Za-z0-9_-'>>*.
21
+ # Punct ::= <<"',.;:?!>>.
22
+ # NotSym ::= '~' | '¬'.
23
+ # AndSym ::= ',' | '∧'.
24
+
25
+
26
+ class SamovarSyntaxError(ValueError):
27
+ pass
28
+
29
+
30
+ def variables_in_cond(cond):
31
+ vars_ = set()
32
+ for expr in cond.exprs:
33
+ expr.term.collect_variables(vars_)
34
+ for key, value in cond.bindings.items():
35
+ vars_.add(Var(key))
36
+ return vars_
37
+
38
+
39
+ class Parser(object):
40
+ def __init__(self, text):
41
+ self.scanner = Scanner(text)
42
+ self.scenario_map = {}
43
+
44
+ def world(self):
45
+ scenarios = []
46
+ while self.scanner.on('scenario'):
47
+ scenario = self.scenario()
48
+ self.scenario_map[scenario.name] = scenario
49
+ scenarios.append(scenario)
50
+ return World(scenarios=scenarios)
51
+
52
+ def scenario(self):
53
+ propositions = []
54
+ rules = []
55
+ goal = None
56
+ self.scanner.expect('scenario')
57
+ self.scanner.check_type('word')
58
+ name = self.scanner.token
59
+ self.scanner.scan()
60
+ self.scanner.expect('{')
61
+ while not self.scanner.on('}'):
62
+ if self.scanner.consume('import'):
63
+ self.scanner.check_type('word')
64
+ from_name = self.scanner.token
65
+ self.scanner.scan()
66
+ from_scenario = self.scenario_map[from_name]
67
+ rules.extend(from_scenario.rules)
68
+ propositions.extend(from_scenario.propositions)
69
+ elif self.scanner.consume('goal'):
70
+ assert goal is None
71
+ goal = self.cond()
72
+ elif self.scanner.on('['):
73
+ rules.append(self.rule())
74
+ else:
75
+ propositions.append(self.proposition())
76
+ self.scanner.consume('.')
77
+ self.scanner.consume(',')
78
+ self.scanner.expect('}')
79
+ return Scenario(name=name, propositions=propositions, rules=rules, goal=goal)
80
+
81
+ def proposition(self):
82
+ return self.term()
83
+
84
+ def rule(self):
85
+ words = []
86
+ pre = self.cond()
87
+ while not self.scanner.on('['):
88
+ words.append(self.word())
89
+ post = self.cond()
90
+
91
+ if post.bindings:
92
+ raise SamovarSyntaxError("Consequences of a rule cannot include a `where` clause")
93
+
94
+ pre_variables = variables_in_cond(pre)
95
+
96
+ words_variables = set(w for w in words if isinstance(w, Var))
97
+ text = ' '.join([str(w) for w in words])
98
+ if '?_' in [w.name for w in words_variables]:
99
+ raise SamovarSyntaxError('Text "{}" contains wildcard'.format(text))
100
+ extra_vars_in_words = words_variables - pre_variables
101
+ if extra_vars_in_words:
102
+ extra_vars = ', '.join([str(v) for v in sorted(extra_vars_in_words)])
103
+ raise SamovarSyntaxError('Text "{}" contains unbound variables: {}'.format(text, extra_vars))
104
+
105
+ post_variables = variables_in_cond(post)
106
+ if '?_' in [w.name for w in post_variables]:
107
+ raise SamovarSyntaxError("Consequences contains wildcard")
108
+ extra_vars_in_post = post_variables - pre_variables
109
+ if extra_vars_in_post:
110
+ extra_vars = ', '.join([str(v) for v in sorted(extra_vars_in_words)])
111
+ raise SamovarSyntaxError("Consequences contains unbound variables: {}".format(extra_vars))
112
+
113
+ return Rule(pre=pre, words=words, post=post)
114
+
115
+ def cond(self):
116
+ exprs = []
117
+ bindings = {}
118
+ self.scanner.expect('[')
119
+ if not self.scanner.on(']') and not self.scanner.on('where'):
120
+ exprs.append(self.expr())
121
+ while self.scanner.consume(',', u'∧'):
122
+ exprs.append(self.expr())
123
+ if self.scanner.consume('where'):
124
+ while not self.scanner.on(']'):
125
+ v = self.var()
126
+ self.scanner.expect('=')
127
+ t = self.term()
128
+ bindings[v.name] = t
129
+ self.scanner.consume(',', u'∧')
130
+ self.scanner.expect(']')
131
+ return Cond(exprs=exprs, bindings=bindings)
132
+
133
+ def expr(self):
134
+ if self.scanner.consume('~', u'¬', '!'):
135
+ return Retract(term=self.term())
136
+ else:
137
+ return Assert(term=self.term())
138
+
139
+ def term(self):
140
+ if self.scanner.on_type('variable'):
141
+ return self.var()
142
+ self.scanner.check_type('word')
143
+ constructor = self.scanner.token
144
+ self.scanner.scan()
145
+ subterms = []
146
+ if self.scanner.consume('('):
147
+ subterms.append(self.term())
148
+ while self.scanner.consume(','):
149
+ subterms.append(self.term())
150
+ self.scanner.expect(')')
151
+ return Term(constructor, *subterms)
152
+
153
+ def word(self):
154
+ if self.scanner.on_type('variable'):
155
+ return self.var()
156
+ self.scanner.check_type('word', 'punct', 'operator')
157
+ constructor = self.scanner.token
158
+ self.scanner.scan()
159
+ return Term(constructor)
160
+
161
+ def var(self):
162
+ self.scanner.check_type('variable')
163
+ name = self.scanner.token
164
+ self.scanner.scan()
165
+ v = Var(name)
166
+ return v
samovar/randomness.py ADDED
@@ -0,0 +1,20 @@
1
+ # Sources of randomness, for Samovar.
2
+
3
+
4
+ class CannedRandomness(object):
5
+ def __init__(self):
6
+ self.counter = 0
7
+ self.limit = 1
8
+
9
+ def advance(self):
10
+ self.counter += 1
11
+ if self.counter > self.limit:
12
+ self.counter = 0
13
+ self.limit += 1
14
+
15
+ def choice(self, iterable):
16
+ length = len(iterable)
17
+ index = self.counter % length
18
+ selection = iterable[index]
19
+ self.advance()
20
+ return selection
samovar/scanner.py ADDED
@@ -0,0 +1,120 @@
1
+ # encoding: UTF-8
2
+
3
+
4
+ # Python 2/3
5
+ try:
6
+ unicode = unicode
7
+ except NameError:
8
+ unicode = str
9
+
10
+
11
+ import re
12
+
13
+
14
+ GREEK = [
15
+ (u'?alpha', u'α'),
16
+ (u'?beta', u'β'),
17
+ (u'?gamma', u'γ'),
18
+ (u'?delta', u'δ'),
19
+ (u'?epsilon', u'ε'),
20
+ (u'?zeta', u'ζ'),
21
+ (u'?theta', u'θ'),
22
+ (u'?iota', u'ι'),
23
+ (u'?kappa', u'κ'),
24
+ (u'?lambda', u'λ'),
25
+ (u'?mu', u'μ'),
26
+ (u'?nu', u'ν'),
27
+ (u'?xi', u'ξ'),
28
+ (u'?omicron', u'ο'),
29
+ (u'?pi', u'π'),
30
+ (u'?rho', u'ρ'),
31
+ (u'?sigma', u'σ'),
32
+ (u'?tau', u'τ'),
33
+ (u'?upsilon', u'υ'),
34
+ (u'?phi', u'φ'),
35
+ (u'?chi', u'χ'),
36
+ (u'?psi', u'ψ'),
37
+ (u'?omega', u'ω'),
38
+ ]
39
+
40
+
41
+ class Scanner(object):
42
+ def __init__(self, text):
43
+ self.text = unicode(text)
44
+ self.token = None
45
+ self.type = None
46
+ self.pos = 0
47
+ self.scan()
48
+
49
+ def near_text(self, length=10):
50
+ return self.text[self.pos:self.pos + length]
51
+
52
+ def scan_pattern(self, pattern, type, token_group=1, rest_group=2):
53
+ pattern = r'(' + pattern + r')'
54
+ regexp = re.compile(pattern, flags=re.DOTALL)
55
+ match = regexp.match(self.text, pos=self.pos)
56
+ if not match:
57
+ return False
58
+ else:
59
+ self.type = type
60
+ self.token = match.group(token_group)
61
+ self.pos += len(self.token)
62
+ return True
63
+
64
+ def scan(self):
65
+ self.scan_pattern(r'[ \t\n\r]*', 'whitespace')
66
+ while self.scan_pattern(r'\/\/.*?[\n\r]', 'comment'):
67
+ self.scan_pattern(r'[ \t\n\r]*', 'whitespace')
68
+ if self.pos >= len(self.text):
69
+ self.token = None
70
+ self.type = 'EOF'
71
+ return
72
+ if self.scan_pattern(u'\\~|→|=|¬|∧|∨', 'operator'):
73
+ return
74
+ # Note that '?' appears in both `variable` and in `punct`.
75
+ # This is acceptable however, because in normal prose, the
76
+ # '?' symbol is never immediately followed by a letter.
77
+ if self.scan_pattern(r'\?[a-zA-Z_]+', 'variable'):
78
+ return
79
+ if self.scan_pattern(r'\,|\.|\;|\:|\?|\!|\"', 'punct'):
80
+ return
81
+ if self.scan_pattern(r'\(|\)|\{|\}|\[|\]', 'bracket'):
82
+ return
83
+ if self.scan_pattern(r"[a-zA-Z_]['a-zA-Z0-9_-]*", 'word'):
84
+ return
85
+ if self.scan_pattern(u'[αβγδεζθικλμνξοπρστυφχψω]', 'variable'):
86
+ for varname, letter in GREEK:
87
+ if letter == self.token:
88
+ self.token = varname
89
+ break
90
+ assert self.token.startswith('?'), repr(self.token)
91
+ return
92
+ if self.scan_pattern(r'.', 'unknown character'):
93
+ return
94
+ else:
95
+ raise AssertionError("this should never happen, self.text=(%s), self.pos=(%s)" % (self.text, self.pos))
96
+
97
+ def expect(self, token):
98
+ if self.token == token:
99
+ self.scan()
100
+ else:
101
+ raise SyntaxError(u"Expected '%s', but found '%s' (near '%s')" %
102
+ (token, self.token, self.near_text()))
103
+
104
+ def on(self, *tokens):
105
+ return self.token in tokens
106
+
107
+ def on_type(self, *types):
108
+ return self.type in types
109
+
110
+ def check_type(self, *types):
111
+ if not self.on_type(*types):
112
+ raise SyntaxError(u"Expected %s, but found %s ('%s') (near '%s')" %
113
+ (types, self.type, self.token, self.near_text()))
114
+
115
+ def consume(self, *tokens):
116
+ if self.token in tokens:
117
+ self.scan()
118
+ return True
119
+ else:
120
+ return False
samovar/terms.py ADDED
@@ -0,0 +1,127 @@
1
+ # encoding: UTF-8
2
+
3
+
4
+ # Python 2/3
5
+ try:
6
+ unicode = unicode
7
+ except NameError:
8
+ unicode = str
9
+
10
+
11
+ class AbstractTerm(object):
12
+ pass
13
+
14
+
15
+ class Term(AbstractTerm):
16
+ def __init__(self, constructor, *subterms):
17
+ self.t = tuple([constructor] + list(subterms))
18
+
19
+ def __lt__(self, other):
20
+ if isinstance(other, Term):
21
+ return self.t < other.t
22
+ else:
23
+ raise TypeError("'<' not supported between instances of 'Term' and '{}'".format(other.__class__.__name__))
24
+
25
+ @property
26
+ def constructor(self):
27
+ return self.t[0]
28
+
29
+ @property
30
+ def subterms(self):
31
+ return self.t[1:]
32
+
33
+ def __str__(self):
34
+ if len(self.subterms) == 0:
35
+ return self.constructor
36
+ return u"%s(%s)" % (self.constructor, ', '.join([unicode(s) for s in self.subterms]))
37
+
38
+ def __repr__(self):
39
+ if self.subterms:
40
+ return "%s(%r, *%r)" % (
41
+ self.__class__.__name__, self.constructor, self.subterms
42
+ )
43
+ else:
44
+ return "%s(%r)" % (
45
+ self.__class__.__name__, self.constructor
46
+ )
47
+
48
+ def __eq__(self, other):
49
+ return isinstance(other, Term) and self.t == other.t
50
+
51
+ def __hash__(self):
52
+ return hash(self.t)
53
+
54
+ def is_atom(self):
55
+ return len(self.subterms) == 0
56
+
57
+ def is_ground(term):
58
+ for subterm in term.subterms:
59
+ if not subterm.is_ground():
60
+ return False
61
+ return True
62
+
63
+ def match(self, term, env, **kwargs):
64
+ """Returns None if no match."""
65
+ if env is None or self.constructor != term.constructor or len(self.subterms) != len(term.subterms):
66
+ return None
67
+ for (subpat, subterm) in zip(self.subterms, term.subterms):
68
+ env = subpat.match(subterm, env, **kwargs)
69
+ return env
70
+
71
+ def subst(self, env):
72
+ return Term(self.constructor, *[subterm.subst(env) for subterm in self.subterms])
73
+
74
+ def collect_atoms(self, atoms):
75
+ if self.is_atom():
76
+ atoms.add(self)
77
+ else:
78
+ for subterm in self.subterms:
79
+ subterm.collect_atoms(atoms)
80
+
81
+ def collect_variables(self, vars_):
82
+ for subterm in self.subterms:
83
+ subterm.collect_variables(vars_)
84
+
85
+
86
+ class Var(AbstractTerm):
87
+ def __init__(self, name):
88
+ self.name = unicode(name)
89
+
90
+ def __str__(self):
91
+ return self.name
92
+
93
+ def __repr__(self):
94
+ return "%s(%r)" % (self.__class__.__name__, self.name)
95
+
96
+ def __eq__(self, other):
97
+ return isinstance(other, Var) and self.name == other.name
98
+
99
+ def __hash__(self):
100
+ return hash(self.name)
101
+
102
+ def is_atom(self):
103
+ return False
104
+
105
+ def is_ground(term):
106
+ return False
107
+
108
+ def match(self, term, env, **kwargs):
109
+ if env is None or self.name == '?_':
110
+ return env
111
+ if self.name in env:
112
+ bound_to = env[self.name]
113
+ return bound_to.match(term, env, **kwargs)
114
+ if kwargs.get('unique_binding'):
115
+ if term in env.values():
116
+ # raise ValueError("Not unique")
117
+ return None
118
+ return dict(list(env.items()) + [(self.name, term)])
119
+
120
+ def subst(self, env):
121
+ return env[self.name]
122
+
123
+ def collect_atoms(self, atoms):
124
+ pass
125
+
126
+ def collect_variables(self, vars_):
127
+ vars_.add(self)
samovar/tests.py ADDED
@@ -0,0 +1,209 @@
1
+ import unittest
2
+ from unittest import TestCase
3
+
4
+ from samovar.ast import Assert, Retract, join_sentence_parts
5
+ from samovar.database import Database
6
+ from samovar.terms import Term, Var
7
+
8
+
9
+ def t(s, *args):
10
+ if s.startswith('?'):
11
+ return Var(s)
12
+ else:
13
+ return Term(s, *args)
14
+
15
+
16
+ def a(t):
17
+ return Assert(term=t)
18
+
19
+
20
+ def r(t):
21
+ return Retract(term=t)
22
+
23
+
24
+ class TermTestCase(TestCase):
25
+ def test_term_basic_properties(self):
26
+ t1 = Term('alice')
27
+ t2 = Term('actor', t1)
28
+ v1 = Var('?A')
29
+ t3 = Term('actor', v1)
30
+
31
+ self.assertTrue(t1.is_atom())
32
+ self.assertFalse(t2.is_atom())
33
+ self.assertFalse(v1.is_atom())
34
+ self.assertFalse(t3.is_atom())
35
+
36
+ self.assertTrue(t1.is_ground())
37
+ self.assertTrue(t2.is_ground())
38
+ self.assertFalse(v1.is_ground())
39
+ self.assertFalse(t3.is_ground())
40
+
41
+ self.assertEqual(t2, Term('actor', Term('alice')))
42
+
43
+ def test_term_match_ground(self):
44
+ t1 = Term('actor', Term('alice'))
45
+ p1 = Term('actor', Term('alice'))
46
+ u = p1.match(t1, {})
47
+ self.assertEqual(u, {})
48
+
49
+ def test_term_no_match_ground(self):
50
+ t1 = Term('actor', Term('alice'))
51
+ p1 = Term('actor', Term('bob'))
52
+ self.assertIsNone(p1.match(t1, {}))
53
+
54
+ def test_term_match_bind_var(self):
55
+ t1 = Term('actor', Term('alice'))
56
+ p1 = Term('actor', Var('?A'))
57
+ e = {}
58
+ u = p1.match(t1, e)
59
+ self.assertEqual(u, {u'?A': Term('alice')})
60
+ self.assertEqual(e, {})
61
+
62
+ def test_term_match_already_bound_var(self):
63
+ t1 = Term('actor', Term('alice'))
64
+ p1 = Term('actor', Var('?A'))
65
+ u = p1.match(t1, {u'?A': Term('alice')})
66
+ self.assertEqual(u, {u'?A': Term('alice')})
67
+
68
+ def test_term_no_match_already_bound_var(self):
69
+ t1 = Term('actor', Term('alice'))
70
+ p1 = Term('actor', Var('?A'))
71
+ u = {u'?A': Term('bob')}
72
+ self.assertIsNone(p1.match(t1, u))
73
+
74
+ def test_term_subst(self):
75
+ t = Term('actor', Var('?A'))
76
+ r = t.subst({u'?A': Term('alice')})
77
+ self.assertEqual(r, Term('actor', Term('alice')))
78
+
79
+
80
+ class RenderTestCase(TestCase):
81
+ def test_join_sentence_parts_1(self):
82
+ self.assertEqual(
83
+ join_sentence_parts(['"', "Hello", ",", '"', "said", "the", "mouse", "."]),
84
+ '"Hello," said the mouse.'
85
+ )
86
+
87
+ def test_join_sentence_parts_2(self):
88
+ self.assertEqual(
89
+ join_sentence_parts(["The", "mouse", "asked", ",", '"', "What", "is", "it", "?", '"']),
90
+ 'The mouse asked, "What is it?"'
91
+ )
92
+
93
+ def test_join_sentence_parts_3(self):
94
+ self.assertEqual(
95
+ join_sentence_parts(["It", "was", "very", ",", "very", "dark", '.']),
96
+ 'It was very, very dark.'
97
+ )
98
+
99
+
100
+ class DatabaseTestCase(unittest.TestCase):
101
+
102
+ def setUp(self):
103
+ self.database = Database([
104
+ t('actor', t('alice')),
105
+ t('actor', t('bob')),
106
+
107
+ t('drink', t('gin')),
108
+
109
+ t('weapon', t('revolver')),
110
+ t('weapon', t('knife')),
111
+ t('weapon', t('club')),
112
+
113
+ t('holding', t('bob'), t('revolver')),
114
+ t('holding', t('alice'), t('gin')),
115
+ t('holding', t('alice'), t('knife')),
116
+ ])
117
+
118
+
119
+ class TestMatchAll(DatabaseTestCase):
120
+
121
+ def assertMatchAll(self, query, result):
122
+ self.assertEqual(self.database.match_all(query, {}), result)
123
+
124
+ def test_match_all(self):
125
+ # Find all actors who are Cody. Since there is no such actor, this will return no matches.
126
+ self.assertMatchAll(
127
+ [a(t('actor', t('cody')))],
128
+ []
129
+ )
130
+ # Find all actors who are Alice. This will return one match, but no bindings.
131
+ self.assertMatchAll(
132
+ [a(t('actor', t('alice')))],
133
+ [{}]
134
+ )
135
+ # Find all drinks. This will return one match, with ?D bound to the result.
136
+ self.assertMatchAll(
137
+ [a(t("drink", t("?D")))],
138
+ [{'?D': Term('gin')}] # there was a match, in which ?D was bound
139
+ )
140
+ # Find all actors.
141
+ self.assertMatchAll(
142
+ [a(t('actor', t('?C')))],
143
+ [{'?C': Term('alice')}, {'?C': Term('bob')}]
144
+ )
145
+ # Find all actors who are holding the revolver.
146
+ self.assertMatchAll(
147
+ [a(t('actor', t('?C'))), a(t('holding', t('?C'), t('revolver')))],
148
+ [{'?C': t('bob')}]
149
+ )
150
+ # Find all actors who are holding a weapon.
151
+ self.assertMatchAll(
152
+ [a(t('actor', t('?C'))), a(t('weapon', t('?W'))), a(t('holding', t('?C'), t('?W')))],
153
+ [{'?W': t('knife'), '?C': t('alice')}, {'?W': t('revolver'), '?C': t('bob')}]
154
+ )
155
+
156
+ def test_match_all_with_unique_binding(self):
157
+ # Find all pairs of actors. Because the actors must be different, there are only 2 matches.
158
+ self.assertMatchAll(
159
+ [a(t('actor', t('?A'))), a(t('actor', t('?B')))],
160
+ [{'?A': Term('alice'), '?B': Term('bob')}, {'?A': Term('bob'), '?B': Term('alice')}]
161
+ )
162
+ # Find all pairs of drinks. Since there is only one, and we can't return (gin,gin),
163
+ # there will be no matches.
164
+ self.assertMatchAll(
165
+ [a(t('drink', t('?A'))), a(t('drink', t('?B')))],
166
+ []
167
+ )
168
+
169
+ def test_match_all_with_negation(self):
170
+ # Find all actors who are not holding the revolver.
171
+ self.assertMatchAll(
172
+ [a(t('actor', t('?C'))), r(t('holding', t('?C'), t('revolver')))],
173
+ [{'?C': t('alice')}]
174
+ )
175
+ # Find all actors who are not holding a weapon. Or rather, all pairs
176
+ # of (actor, weapon) where the actor is not holding that weapon.
177
+ self.assertMatchAll(
178
+ [a(t('actor', t('?C'))), a(t('weapon', t('?W'))), r(t('holding', t('?C'), t('?W')))],
179
+ [
180
+ {'?W': t('club'), '?C': t('alice')},
181
+ {'?W': t('revolver'), '?C': t('alice')},
182
+ {'?W': t('club'), '?C': t('bob')},
183
+ {'?W': t('knife'), '?C': t('bob')},
184
+ ]
185
+ )
186
+ # Note that we can't say "Find all actors who aren't Alice".
187
+ # We can say this:
188
+ self.assertMatchAll(
189
+ [a(t('actor', t('?C'))), r(t('actor', t('alice')))],
190
+ []
191
+ )
192
+ # ... but what this is saying is "Find all actors if Alice isn't an actor."
193
+
194
+ # For a one-off case, we can do something like this:
195
+ self.database.add(t('is_alice', t('alice')))
196
+ self.assertMatchAll(
197
+ [a(t('actor', t('?C'))), r(t('is_alice', t('?C')))],
198
+ [{'?C': t('bob')}]
199
+ )
200
+
201
+ # For the general case, we'll need to think about equality tests.
202
+
203
+ # Note also that we can't search on negative clauses with free variables:
204
+ with self.assertRaises(KeyError):
205
+ self.database.match_all([a(t('actor', t('?C'))), r(t('weapon', t('?W')))], {})
206
+
207
+
208
+ if __name__ == '__main__':
209
+ unittest.main()
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.1
2
+ Name: samovar-py
3
+ Version: 0.6
4
+ Summary: Samovar: model worlds using propositions and run simulations in them
5
+ Author-email: Chris Pressey <packages@catseye.tc>
6
+ License: This is free and unencumbered software released into the public domain.
7
+
8
+ Anyone is free to copy, modify, publish, use, compile, sell, or
9
+ distribute this software, either in source code form or as a compiled
10
+ binary, for any purpose, commercial or non-commercial, and by any
11
+ means.
12
+
13
+ In jurisdictions that recognize copyright laws, the author or authors
14
+ of this software dedicate any and all copyright interest in the
15
+ software to the public domain. We make this dedication for the benefit
16
+ of the public at large and to the detriment of our heirs and
17
+ successors. We intend this dedication to be an overt act of
18
+ relinquishment in perpetuity of all present and future rights to this
19
+ software under copyright law.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ For more information, please refer to <http://unlicense.org/>
30
+
31
+ Project-URL: website, https://catseye.tc/node/Samovar
32
+ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 2.7
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Topic :: Artistic Software
37
+ Classifier: Topic :: Games/Entertainment :: Simulation
38
+ Requires-Python: >=2.7,>=3.5
39
+ Description-Content-Type: text/markdown
40
+
41
+ Samovar
42
+ =======
43
+
44
+ Version 0.6 | _Entry_ [@ catseye.tc](https://catseye.tc/node/Samovar)
45
+ | _See also:_ [The League of Extraordinarily Dull Gentlemen](https://github.com/catseye/NaNoGenMo-Entries-2018/tree/master/league#readme)
46
+ ∘ [The Swallows](https://github.com/catseye/The-Swallows#readme)
47
+
48
+ - - - -
49
+
50
+ **Samovar** is a domain-specific language (DSL) for modelling a world using
51
+ propositions (facts), and possible events that can occur based on those facts,
52
+ changing them.
53
+
54
+ Here is a short example of a Samovar description:
55
+
56
+ scenario IgnatzWithBrick {
57
+
58
+ [actor(?A),item(?I),~holding(?A,?I)] ?A picks up the ?I. [holding(?A,?I)]
59
+ [actor(?A),item(?I),holding(?A,?I)] ?A puts down the ?I. [~holding(?A,?I)]
60
+
61
+ actor(Ignatz).
62
+ item(brick).
63
+
64
+ goal [].
65
+ }
66
+
67
+ And an implementation of Samovar could take this scenario and use it to,
68
+ among other things, generate textual descriptions of chains of events like
69
+
70
+ Ignatz picks up the brick. Ignatz puts down the brick.
71
+
72
+ Of course, this is a very simple example. (It doesn't even prevent two
73
+ actors from picking up the same item at the same time!) For more complex
74
+ examples, and a fuller description of the language, see
75
+ [doc/Samovar.md](doc/Samovar.md), which also serves as a test suite.
76
+
77
+ ### Implementation
78
+
79
+ The reference implementation of Samovar, `samovar`, is written in Python.
80
+ It can run under either Python 2.7 or Python 3.x (tested with 3.5 or higher.)
81
+
82
+ ### Discussion
83
+
84
+ This looks like logic programming but the internals are actually much simpler,
85
+ and what it's doing is not logical inference.
86
+
87
+ The internals are far simpler than an inference engine or a theorem
88
+ prover: there are no logical rules in the database, only propositions, so
89
+ they can be selected by simple pattern-matching rather than full unification.
90
+
91
+ I originally described it as an "assertion-retraction engine", which could
92
+ be thought of as a highly stylized form of Prolog programming. Alternately,
93
+ it could be thought of as assigning preconditions and postconditions, like
94
+ in [Hoare logic][], to actions in a world-model. Instead of constructing a
95
+ proof, though, we simply chain actions together, by selecting any next one
96
+ whose preconditions hold, and altering the world so that its postconditions hold.
97
+
98
+ In (I think) autumn 2019, I came across the concept of a [production system][],
99
+ and I realized that is basically what Samovar implements. Researching it
100
+ further, I discovered [CLIPS][], a production system built in 1985,
101
+ and I realized that Samovar is not essentially different from an
102
+ inefficiently-implemented, stripped-down version of CLIPS.
103
+
104
+ The main innovation in Samovar is the ability to "narrate" the choices the
105
+ production system makes, with human-readable text.
106
+
107
+ This isn't too surprising, since my goal with Samovar was to make an expressive
108
+ language, a syntax in which the author could write a story generator without
109
+ the undue effort of switching context between prose and code. Picking logical
110
+ propositions for the "code" and placing them before and after each fragment of
111
+ prose was a reasonably "ergonomic" choice, and happens to coincide with the
112
+ structure of a production system.
113
+
114
+ [Hoare logic]: https://en.wikipedia.org/wiki/Hoare_logic
115
+ [production system]: https://en.wikipedia.org/wiki/Production_system_(computer_science)
116
+ [CLIPS]: https://en.wikipedia.org/wiki/CLIPS
117
+
118
+ ### TODO
119
+
120
+ * Maybe allow variables to be notated so that they can bind reflexively,
121
+ e.g. `?*A looks at ?*B` can bind both variables to Alice.
122
+ * Make `?_` work such that you can say `¬holding(?_, club)` to mean
123
+ "if no one is holding the club".
@@ -0,0 +1,19 @@
1
+ samovar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ samovar/ast.py,sha256=AFLiqpr9bBkgQ84UeQhV06nWcA17n41kdIoowpJOOew,1509
3
+ samovar/database.py,sha256=eV0x8JGjAiUq697YvkK0gjIzj8OJMln6IWpT8Dsqdug,2796
4
+ samovar/main.py,sha256=RmUtRT3O7R_IumHDJT6xJel1GTtCrWwqYvN-6DMTHZc,4934
5
+ samovar/parser.py,sha256=bYrtRhXHTAkEj7g9z7Lfua85VcRpzpFxg84_LN4BBwY,5847
6
+ samovar/randomness.py,sha256=4scvXciSZ7W2hINWnLcAZf0ov4RYO8BJ7Zr4tzhnx6Q,475
7
+ samovar/scanner.py,sha256=V9bYcY0n_oFUqk87SBBToE-XtTALcXtmE3RmiZoab_s,3575
8
+ samovar/terms.py,sha256=aYTEJynkCu_ecDm7WpETT_XSBl-Wl4JPEV7unn5AqDE,3360
9
+ samovar/tests.py,sha256=T-clqS77aRloIt5tkm56ffeY05oYIB0JUeUWyjqMUj8,7017
10
+ samovar/generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ samovar/generators/base.py,sha256=-oeffjcGkVyDr0ltIY9W0OUQrG84LYbqJWZiWTkQ4Qk,1799
12
+ samovar/generators/breadthfirst.py,sha256=JA9ZcGA6Nj0YM8f5QQPVgL04l-uCmfE6ntlo65B25B8,2181
13
+ samovar/generators/depthfirst.py,sha256=7HfkMKb0RUiObC2Y9nzY7U2bg7z33Vtcx1dKhGVv4d4,2605
14
+ samovar/generators/stochastic.py,sha256=6ca1OjpjocCBInq81tuKv_HLBgbLwgN_cZhaSrizbw0,2218
15
+ samovar_py-0.6.dist-info/METADATA,sha256=tf1Z8B5hHVEi51MECtnajfjbse8st1lHVIxqXOEEC0I,5656
16
+ samovar_py-0.6.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
17
+ samovar_py-0.6.dist-info/entry_points.txt,sha256=tpU5DZJ34s4hpNFCobyu6E-SdWQyxfQBy_-oMZFqMxk,50
18
+ samovar_py-0.6.dist-info/top_level.txt,sha256=78kn4qDuoyXOHtnHgEl6PtZaxBAQ7Basrn2mBbW8i_Y,8
19
+ samovar_py-0.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ samovar = samovar.main:sys_main
@@ -0,0 +1 @@
1
+ samovar