Merge pull request #9 from catseye/develop-0.2
Develop 0.2
Chris Pressey authored 6 years ago
GitHub committed 6 years ago
0 | History of Samovar | |
1 | ================== | |
2 | ||
3 | ### Version 0.2 | |
4 | ||
5 | Improved during a "sprint" in the first half of November 2018, | |
6 | for NaNoGenMo 2018. | |
7 | ||
8 | #### Language | |
9 | ||
10 | * Added tests: a Falderal test suite, as well as unit tests. | |
11 | * Variables no longer need to be Greek letters; they can be | |
12 | alphanumeric identifiers of any length that begin with `?`. | |
13 | * Traditional logical connectives can be used in expressions. | |
14 | * "Scenarios" instead of "situations"; a scenario is not | |
15 | divided into sections, it can have a `goal`, and it can | |
16 | `import` other scenarios. | |
17 | * When matching a rule, the same term cannot be bound to | |
18 | two different variables. | |
19 | * Ability to "pre-bind" a variable to a term, in a rule's | |
20 | `where` clause. | |
21 | * Removed functions, after demonstrating they're not needed. | |
22 | * Clearer expectation that punctuation should be retained when | |
23 | it appears in a rule's words. | |
24 | ||
25 | #### Reference implementation | |
26 | ||
27 | * Ability to set the random seed with `--seed`. | |
28 | * Improved algorithm for matching rules against propositions; | |
29 | uses depth-first recursive pattern-matching to find matching | |
30 | rules. Bindings also treated as immutable. This led to a | |
31 | significant performance improvement (the prototype was ugly.) | |
32 | * Implemented the AST with namedtuples, for slight performance | |
33 | improvement. | |
34 | * Able to output generated events as JSON. | |
35 | * `--generate-scenarios`, -`-min-events`, `--max-events`, | |
36 | `--lengten-factor` command-line options. | |
37 | * Support for running under Python 3. | |
38 | ||
39 | ### Version 0.1 | |
40 | ||
41 | The initial prototype, thrown together in October 2016. |
0 | 0 | Samovar |
1 | 1 | ======= |
2 | 2 | |
3 | *Version 0.1. Subject to change in backwards-incompatible ways.* | |
3 | *Version 0.2. Subject to change in backwards-incompatible ways.* | |
4 | 4 | |
5 | Samovar is a DSL for world-modeling using predicates rather than explicit objects. | |
5 | Samovar is a DSL for modelling a world using propositions (facts), and possible | |
6 | events that can occur based on those facts, changing them. | |
6 | 7 | |
7 | The remainder of this document will probably be trying to explain what I mean by | |
8 | that. | |
8 | Here is a short example of a Samovar description: | |
9 | 9 | |
10 | It could be thought of as an "assertion-retraction engine", which itself could be | |
11 | thought of as a very stilted style of Prolog programming plus some syntactic | |
10 | scenario IgnatzWithBrick { | |
11 | ||
12 | [actor(?A),item(?I),~holding(?A,?I)] ?A picks up the ?I. [holding(?A,?I)] | |
13 | [actor(?A),item(?I),holding(?A,?I)] ?A puts down the ?I. [~holding(?A,?I)] | |
14 | ||
15 | actor(Ignatz). | |
16 | item(brick). | |
17 | ||
18 | goal []. | |
19 | } | |
20 | ||
21 | And an implementation of Samovar could take this scenario and use it to, | |
22 | among other things, generate textual descriptions of chains of events like | |
23 | ||
24 | Ignatz picks up the brick. Ignatz puts down the brick. | |
25 | ||
26 | Of course, this is a very simple example. (It doesn't even prevent two | |
27 | actors from picking up the same item at the same time!) For more complex | |
28 | examples, and a fuller description of the language, see | |
29 | [doc/Samovar.md](doc/Samovar.md), which doubles as a test suite. | |
30 | ||
31 | ### Discussion | |
32 | ||
33 | This looks like logic programming but the internals are actually much simpler. | |
34 | ||
35 | Samovar could be described as an "assertion-retraction engine", which itself could | |
36 | be thought of as a highly stylized form of Prolog programming plus some syntactic | |
12 | 37 | sugar. |
13 | 38 | |
14 | 39 | Alternately, it could be thought of as assigning preconditions and postconditions, |
15 | like you would find in a program proof, to actions in a world-model. Instead of | |
40 | like you would find in [Hoare logic][], to actions in a world-model. Instead of | |
16 | 41 | proving that the action satisfies the conditions, though, we simply assume it |
17 | 42 | does, and use the conditions to chain actions together in a sensible order. |
18 | 43 | |
19 | A Samovar world is an immutable set of rules which operate on a mutable set of | |
20 | facts. Each rule looks like | |
44 | But really, the internals are far simpler than an inference engine or a theorem | |
45 | prover: there are no logical rules in the database, only propositions, so | |
46 | they can be selected by simple pattern-matching rather than full unification. | |
21 | 47 | |
22 | [A] X [B] | |
48 | [Hoare logic]: https://en.wikipedia.org/wiki/Hoare_logic | |
23 | 49 | |
24 | and means "If A holds, then X is a possible action to take, and if you do take it, | |
25 | you must make B hold afterwards." | |
50 | ### TODO | |
26 | 51 | |
27 | By "hold" we mean "can unify with the current set of facts." | |
28 | ||
29 | As an example, | |
30 | ||
31 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
32 | ||
33 | Which can be read "If A is an actor and B is an item and A is not holding B, then | |
34 | one possible action is to say 'A picks up the B' and assert that A is now holding B." | |
35 | ||
36 | We can add a complementary rule: | |
37 | ||
38 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
39 | ||
40 | And we can package this all into a world-description: | |
41 | ||
42 | rules | |
43 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
44 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
45 | end | |
46 | situations | |
47 | [actor(Ignatz),item(brick)] | |
48 | end | |
49 | ||
50 | And an implementation of Samovar can take this world-description and use it to, | |
51 | among other things, generate chains of events like | |
52 | ||
53 | Ignatz picks up the brick. Ignatz puts down the brick. | |
54 | ||
55 | Of course, this is a very simple example. A more complex example might have | |
56 | more actors, more items, and more rules (for example, that two actors cannot | |
57 | be holding the same item at the same time.) | |
52 | * Implement a "wildcard" variable that will match anything *without* binding it. | |
53 | * Consider what it would take to add a predicate that evaluates to whether | |
54 | a given action has been taken previously or not. | |
55 | * Consider macros. | |
56 | * Consider making "wildcard" work such that you can say `¬holding(?_, club)` | |
57 | to mean "if no one is holding the club". | |
58 | * Statically check that the 2nd cond in a rule has no "where" clause | |
59 | * Commas after bindings in "where" | |
60 | * Statically check that every var in the 2nd cond was bound in the 1st cond |
3 | 3 | """\ |
4 | 4 | samovar {option} input.samovar |
5 | 5 | |
6 | Driver script for Samovr assertion-retraction engine. | |
6 | Driver script for Samovar assertion-retraction engine. | |
7 | 7 | """ |
8 | 8 | |
9 | 9 | from os.path import realpath, dirname, join |
11 | 11 | |
12 | 12 | sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src')) |
13 | 13 | |
14 | from argparse import ArgumentParser | |
14 | 15 | import codecs |
15 | from optparse import OptionParser | |
16 | import json | |
17 | import random | |
16 | 18 | |
17 | 19 | from samovar.parser import Parser |
18 | 20 | from samovar.generator import Generator |
19 | 21 | |
20 | 22 | |
21 | if __name__ == '__main__': | |
23 | def generate_fifty_thousand_words(): | |
24 | with codecs.open('eg/chairs.samovar', 'r', encoding='UTF-8') as f: | |
25 | text = f.read() | |
26 | p = Parser(text) | |
27 | ast = p.world() | |
28 | random.seed(0) | |
29 | g = Generator(ast, ast.scenarios[0]) | |
30 | g.generate_events(8000) | |
22 | 31 | |
23 | optparser = OptionParser(__doc__) | |
24 | optparser.add_option("--debug", | |
25 | action="store_true", | |
26 | help="Show state before and after each move") | |
27 | optparser.add_option("--dump-ast", | |
32 | ||
33 | def main(args): | |
34 | argparser = ArgumentParser() | |
35 | ||
36 | argparser.add_argument('input_files', nargs='+', metavar='FILENAME', type=str, | |
37 | help='Source files containing the scenario descriptions' | |
38 | ) | |
39 | argparser.add_argument("--debug", action="store_true", | |
40 | help="Show state before and after each move" | |
41 | ) | |
42 | argparser.add_argument("--verbose", action="store_true", | |
43 | help="Show some progress information" | |
44 | ) | |
45 | argparser.add_argument("--dump-ast", | |
28 | 46 | action="store_true", |
29 | 47 | help="Just show the AST and stop") |
30 | optparser.add_option("--length", | |
31 | type=int, default=0, | |
32 | help="If given, generate this many events for each situation") | |
33 | optparser.add_option("--words", | |
34 | type=int, default=0, | |
35 | help="If given, generate each situation til this many words") | |
36 | (options, args) = optparser.parse_args(sys.argv[1:]) | |
48 | argparser.add_argument("--generate-scenarios", | |
49 | type=str, default=None, | |
50 | help="If given, generate only these scenarios") | |
51 | argparser.add_argument("--min-events", | |
52 | type=int, default=1, | |
53 | help="Generate at least this many events for each scenario") | |
54 | argparser.add_argument("--max-events", | |
55 | type=int, default=1000000, | |
56 | help="Assume something's gone wrong if more than this many events are generated") | |
57 | argparser.add_argument("--lengthen-factor", | |
58 | type=float, default=2.0, | |
59 | help="When scenario goal was not met, multiply number of events to generate by this") | |
60 | argparser.add_argument("--output-type", | |
61 | choices=['naive-text', 'events-json', 'scenarios-json'], | |
62 | default='naive-text', | |
63 | help="Specify what to output and in what format") | |
64 | argparser.add_argument("--seed", | |
65 | type=int, default=None, | |
66 | help="Set random seed (to select moves deterministically)") | |
67 | argparser.add_argument("--profile", | |
68 | action="store_true", | |
69 | help="Run cProfile on standard 'heavy load' case and exit") | |
70 | ||
71 | options = argparser.parse_args(args) | |
72 | ||
73 | if options.profile: | |
74 | import cProfile | |
75 | cProfile.run('generate_fifty_thousand_words()') | |
76 | sys.exit(0) | |
37 | 77 | |
38 | 78 | text = '' |
39 | for arg in args: | |
79 | for arg in options.input_files: | |
40 | 80 | with codecs.open(arg, 'r', encoding='UTF-8') as f: |
41 | 81 | text += f.read() |
42 | 82 | |
43 | 83 | p = Parser(text) |
44 | 84 | ast = p.world() |
45 | 85 | if options.dump_ast: |
46 | print ast | |
86 | print(ast) | |
47 | 87 | sys.exit(0) |
48 | g = Generator(ast, debug=options.debug) | |
49 | if options.length > 0: | |
50 | events = g.generate_events(options.length) | |
51 | elif options.words > 0: | |
52 | events = g.generate_words(options.words) | |
53 | for e in events: | |
54 | sys.stdout.write("%s " % e) | |
55 | sys.stdout.write("\n") | |
88 | if options.seed is not None: | |
89 | random.seed(options.seed) | |
90 | ||
91 | event_buckets = [] | |
92 | for n, scenario in enumerate(ast.scenarios): | |
93 | if options.verbose: | |
94 | sys.stderr.write("{}. {}\n".format(n, scenario.name)) | |
95 | if scenario.goal is None: | |
96 | continue | |
97 | if options.generate_scenarios is not None and scenario.name not in options.generate_scenarios: | |
98 | continue | |
99 | g = Generator(ast, scenario, debug=options.debug, verbose=options.verbose) | |
100 | events = g.generate_events(options.min_events, options.max_events, options.lengthen_factor) | |
101 | event_buckets.append(events) | |
102 | ||
103 | if options.output_type == 'naive-text': | |
104 | for b in event_buckets: | |
105 | for e in b: | |
106 | sys.stdout.write("%s\n" % e) | |
107 | sys.stdout.write("\n") | |
108 | elif options.output_type == 'events-json': | |
109 | def jsonify_bucket(b): | |
110 | return [e.to_json() for e in b] | |
111 | jsonified_buckets = [jsonify_bucket(b) for b in event_buckets] | |
112 | sys.stdout.write(json.dumps(jsonified_buckets, indent=4, sort_keys=True)) | |
113 | elif options.output_type == 'scenarios-json': | |
114 | raise NotImplementedError | |
115 | else: | |
116 | raise NotImplementedError | |
117 | ||
118 | ||
119 | if __name__ == '__main__': | |
120 | main(sys.argv[1:]) |
0 | Samovar | |
1 | ======= | |
2 | ||
3 | This is a literate test suite for Samovar, in [Falderal][] format. | |
4 | It describes Samovar, and includes examples inline, which consist | |
5 | of valid Samovar descriptions and what you might expect from | |
6 | running them. | |
7 | ||
8 | Falderal can actually run these examples and check that they actually | |
9 | produce these results, so these examples serve as tests. | |
10 | ||
11 | However, Samovar only specifies the declarative meaning of a | |
12 | Samovar description, not the operational aspect. An implementation | |
13 | of Samovar is allowed to do pretty much whatever it likes. | |
14 | However, there are certain behaviours that many Samovar implementations | |
15 | (and in particular, the reference implementation) would be reasonably | |
16 | expected to support, and it is this behaviour which these examples | |
17 | will illustrate. | |
18 | ||
19 | [Falderal]: http://catseye.tc/node/Falderal | |
20 | ||
21 | -> Tests for functionality "Run Samovar Simulation" | |
22 | ||
23 | -> Functionality "Run Samovar Simulation" is implemented by | |
24 | -> shell command | |
25 | -> "bin/samovar %(test-body-file) --min-events 4 --seed 0" | |
26 | ||
27 | Basic Syntax | |
28 | ------------ | |
29 | ||
30 | A minimally valid Samovar description looks like this. | |
31 | (The `===>` is not part of the Samovar description. It | |
32 | indicates what output we would expect from this. In this case, | |
33 | nothing.) | |
34 | ||
35 | scenario A {} | |
36 | ||
37 | ===> | |
38 | ||
39 | You can include comments with `//`. | |
40 | ||
41 | // This is my minimal Samovar description. | |
42 | scenario A {} | |
43 | ||
44 | ===> | |
45 | ||
46 | The name of a scenario must begin with a letter or underscore, | |
47 | and can consist of letters, numbers, hyphens, underscores, and | |
48 | apostrophes. | |
49 | ||
50 | The same rules apply to most other "words" appearing in a Samovar | |
51 | description. | |
52 | ||
53 | scenario Pin_afore-isn't-1000 { | |
54 | this-is-a-constructor(this-is-an-atom). | |
55 | } | |
56 | ||
57 | ===> | |
58 | ||
59 | Basic Semantics | |
60 | --------------- | |
61 | ||
62 | The basic unit of a Samovar world is a scenario. Inside a scenario, | |
63 | facts are defined with _propositions_, and possible events are | |
64 | defined with _event rules_. Each event rule looks like | |
65 | ||
66 | [A] X [B] | |
67 | ||
68 | which can be read | |
69 | ||
70 | > If A holds, then X is a possible action to take, and if you do take it, | |
71 | > you must make B hold afterwards. | |
72 | ||
73 | By "hold" we mean "matches the current set of facts." | |
74 | ||
75 | As an example, | |
76 | ||
77 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
78 | ||
79 | Which can be read | |
80 | ||
81 | > If α is an actor and β is an item and α is not holding β, then one possible | |
82 | > action is to write out 'α picks up the β' and assert that α is now holding β. | |
83 | ||
84 | We can add a complementary rule: | |
85 | ||
86 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
87 | ||
88 | And we can package this all into a scenario: | |
89 | ||
90 | scenario IgnatzWithBrick { | |
91 | ||
92 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
93 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
94 | ||
95 | actor(Ignatz). | |
96 | item(brick). | |
97 | ||
98 | goal []. | |
99 | } | |
100 | ===> Ignatz picks up the brick. | |
101 | ===> Ignatz puts down the brick. | |
102 | ===> Ignatz picks up the brick. | |
103 | ===> Ignatz puts down the brick. | |
104 | ||
105 | Scenarios | |
106 | --------- | |
107 | ||
108 | The basic unit of a Samovar world is a scenario. A scenario may contain | |
109 | any number of propositions and event rules, and an optional goal, in any order. | |
110 | ||
111 | scenario IgnatzWithBrick { | |
112 | ||
113 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
114 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
115 | ||
116 | actor(Ignatz). | |
117 | item(brick). | |
118 | ||
119 | goal []. | |
120 | } | |
121 | ===> Ignatz picks up the brick. | |
122 | ===> Ignatz puts down the brick. | |
123 | ===> Ignatz picks up the brick. | |
124 | ===> Ignatz puts down the brick. | |
125 | ||
126 | A source file may contain more than one scenario. By default, our | |
127 | implementation of Samovar runs a simulation on each of the scenarios | |
128 | that has a goal defined, even if that goal is empty. | |
129 | ||
130 | scenario MollyWithBrick { | |
131 | ||
132 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
133 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
134 | ||
135 | actor(Molly). | |
136 | item(brick). | |
137 | ||
138 | } | |
139 | ||
140 | scenario IgnatzWithBrick { | |
141 | ||
142 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
143 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
144 | ||
145 | actor(Ignatz). | |
146 | item(brick). | |
147 | ||
148 | goal []. | |
149 | } | |
150 | ===> Ignatz picks up the brick. | |
151 | ===> Ignatz puts down the brick. | |
152 | ===> Ignatz picks up the brick. | |
153 | ===> Ignatz puts down the brick. | |
154 | ||
155 | Scenarios can import the event rules and propositions from other scenarios. | |
156 | This makes a scenario a good place to collect a setting, or a group of | |
157 | characters who will appear together in scenes. These "library" scenarios | |
158 | should have no goal, as we don't want to generate simulations for them. | |
159 | ||
160 | scenario ItemRules { | |
161 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
162 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
163 | } | |
164 | scenario Actors { | |
165 | actor(Ignatz). | |
166 | } | |
167 | scenario Brickyard { | |
168 | item(brick). | |
169 | } | |
170 | scenario Main { | |
171 | import ItemRules. | |
172 | import Actors. | |
173 | import Brickyard. | |
174 | goal []. | |
175 | } | |
176 | ===> Ignatz picks up the brick. | |
177 | ===> Ignatz puts down the brick. | |
178 | ===> Ignatz picks up the brick. | |
179 | ===> Ignatz puts down the brick. | |
180 | ||
181 | There is nothing stopping an implementation from allowing a Samovar | |
182 | description to be spread over multiple source files, but there is no | |
183 | facility to reference one source file from another in Samovar, so how | |
184 | they are located and collected is up to the implementation. | |
185 | ||
186 | Goals | |
187 | ----- | |
188 | ||
189 | A scenario is run until it meets the goal. How it meets the goal | |
190 | is up to the implementation. Our implementation generates events | |
191 | randomly, until it comes up with a series of events wherein the | |
192 | goal is met, generating more events each time. | |
193 | ||
194 | A goal of `[]`, as above, is trivially met. | |
195 | ||
196 | Generation of events does not stop immediately once the goal is | |
197 | met. A number of events are generated, and then the check is made. | |
198 | ||
199 | scenario UntilHoldBrick { | |
200 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
201 | [actor(α),item(β),holding(α,β)] α puts down the β. [~holding(α,β)] | |
202 | actor(Ignatz). | |
203 | item(brick). | |
204 | item(oilcan). | |
205 | goal [holding(Ignatz,brick)]. | |
206 | } | |
207 | ===> Ignatz picks up the brick. | |
208 | ===> Ignatz puts down the brick. | |
209 | ===> Ignatz picks up the oilcan. | |
210 | ===> Ignatz picks up the brick. | |
211 | ||
212 | Event rules | |
213 | ----------- | |
214 | ||
215 | An event may be selected if its pattern matches the current set of | |
216 | facts. | |
217 | ||
218 | The text inside the event rule is typically expanded with the values | |
219 | that the pattern variables matched. | |
220 | ||
221 | scenario UntilHoldBrick { | |
222 | [actor(α),item(β),~holding(α,β)] α picks up the β. [holding(α,β)] | |
223 | actor(Ignatz). | |
224 | item(brick). | |
225 | goal [holding(Ignatz,brick)]. | |
226 | } | |
227 | ===> Ignatz picks up the brick. | |
228 | ||
229 | The text may contain punctuation. | |
230 | ||
231 | scenario UntilHoldBrick { | |
232 | [actor(α),item(β),~holding(α,β)] "What a lovely β this is!" says α, picking it up. [holding(α,β)] | |
233 | actor(Ignatz). | |
234 | item(brick). | |
235 | goal [holding(Ignatz,brick)]. | |
236 | } | |
237 | ===> "What a lovely brick this is!" says Ignatz, picking it up. | |
238 | ||
239 | scenario UntilHoldBrick { | |
240 | [actor(?A),item(?I),~holding(?A,?I)] "What a lovely ?I this is!" says ?A, picking it up. [holding(?A,?I)] | |
241 | actor(Ignatz). | |
242 | item(brick). | |
243 | goal [holding(Ignatz,brick)]. | |
244 | } | |
245 | ===> "What a lovely brick this is!" says Ignatz, picking it up. | |
246 | ||
247 | Punctuation should be preserved sensibly. | |
248 | ||
249 | scenario UntilHoldBrick { | |
250 | [actor(α),item(β),~holding(α,β)] "β, don't you know?" says α, picking it up. [holding(α,β)] | |
251 | actor(Ignatz). | |
252 | item(brick). | |
253 | goal [holding(Ignatz,brick)]. | |
254 | } | |
255 | ===> "brick, don't you know?" says Ignatz, picking it up. | |
256 | ||
257 | scenario UntilHoldBrick { | |
258 | [actor(?A),item(?I),~holding(?A,?I)] "?I, don't you know?" says ?A, picking it up. [holding(?A,?I)] | |
259 | actor(Ignatz). | |
260 | item(brick). | |
261 | goal [holding(Ignatz,brick)]. | |
262 | } | |
263 | ===> "brick, don't you know?" says Ignatz, picking it up. | |
264 | ||
265 | An event rule may come with some variables pre-bound. | |
266 | ||
267 | scenario UntilHoldBrick { | |
268 | [actor(?A),item(?I),~holding(?A,?I) where ?I=brick] ?A picked up the ?I. [holding(?A,?I)] | |
269 | actor(Ignatz). | |
270 | item(brick). | |
271 | item(banana). | |
272 | goal [holding(Ignatz,brick)]. | |
273 | } | |
274 | ===> Ignatz picked up the brick. | |
275 | ||
276 | chairs | |
277 | ------ | |
278 | ||
279 | scenario Chairs { | |
280 | ||
281 | [actor(ρ)∧¬sitting(ρ)] | |
282 | ρ walks around the room. | |
283 | [] | |
284 | ||
285 | [actor(ρ)∧¬sitting(ρ)∧nearby(κ)∧empty(κ)] | |
286 | ρ sits down in the κ. | |
287 | [sitting(ρ)∧in(ρ,κ)∧¬empty(κ)] | |
288 | ||
289 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
290 | ρ leans back in the κ. | |
291 | [] | |
292 | ||
293 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
294 | ρ gets up and stretches. | |
295 | [¬sitting(ρ)∧¬in(ρ,κ)∧empty(κ)] | |
296 | ||
297 | actor(Hastings). | |
298 | actor(Petersen). | |
299 | actor(Wembley). | |
300 | nearby(chair). empty(chair). | |
301 | nearby(recliner). | |
302 | empty(recliner). | |
303 | nearby(sofa). | |
304 | empty(sofa). | |
305 | ||
306 | goal []. | |
307 | } | |
308 | ===> Wembley sits down in the recliner. | |
309 | ===> Wembley leans back in the recliner. | |
310 | ===> Hastings sits down in the chair. | |
311 | ===> Petersen sits down in the sofa. | |
312 | ||
313 | ||
314 | no need for functions | |
315 | --------------------- | |
316 | ||
317 | Samovar 0.1 had functions, but they were removed because they | |
318 | were not necessary. If you want to look up a property of | |
319 | some thing, you can just pattern-match for it. The example was | |
320 | ||
321 | their(Alice) → her | |
322 | their(Bob) → his | |
323 | ||
324 | but we can just say | |
325 | ||
326 | scenario ScratchesHead { | |
327 | ||
328 | [actor(ρ),possessive(ρ,ξ)] | |
329 | ρ scratches ξ head. | |
330 | [] | |
331 | ||
332 | actor(Alice). | |
333 | possessive(Alice, her). | |
334 | actor(Bob). | |
335 | possessive(Bob, his). | |
336 | ||
337 | goal []. | |
338 | } | |
339 | ===> Alice scratches her head. | |
340 | ===> Alice scratches her head. | |
341 | ===> Bob scratches his head. | |
342 | ===> Bob scratches his head. | |
343 | ||
344 | This loses the nice property of the function name being a readable | |
345 | placeholder in the sentence, but you can now use named variables | |
346 | instead: | |
347 | ||
348 | scenario ScratchesHead { | |
349 | ||
350 | [actor(?Actor),possessive(?Actor,?their)] | |
351 | ?Actor scratches ?their head. | |
352 | [] | |
353 | ||
354 | actor(Alice). | |
355 | possessive(Alice, her). | |
356 | actor(Bob). | |
357 | possessive(Bob, his). | |
358 | ||
359 | goal []. | |
360 | } | |
361 | ===> Alice scratches her head. | |
362 | ===> Alice scratches her head. | |
363 | ===> Bob scratches his head. | |
364 | ===> Bob scratches his head. |
0 | rules | |
0 | scenario Chairs { | |
1 | 1 | |
2 | [actor(ρ),~sitting(ρ)] | |
3 | ρ walks around the room. | |
4 | [] | |
2 | [actor(ρ)∧¬sitting(ρ)] | |
3 | ρ walks around the room. | |
4 | [] | |
5 | 5 | |
6 | [actor(ρ),~sitting(ρ),nearby(κ),empty(κ)] | |
7 | ρ sits down in the κ. | |
8 | [sitting(ρ),in(ρ,κ),~empty(κ)] | |
6 | [actor(ρ)∧¬sitting(ρ)∧nearby(κ)∧empty(κ)] | |
7 | ρ sits down in the κ. | |
8 | [sitting(ρ)∧in(ρ,κ)∧¬empty(κ)] | |
9 | 9 | |
10 | [actor(ρ),sitting(ρ),in(ρ,κ)] | |
11 | ρ leans back in the κ. | |
12 | [] | |
10 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
11 | ρ leans back in the κ. | |
12 | [] | |
13 | 13 | |
14 | [actor(ρ),sitting(ρ),in(ρ,κ)] | |
15 | ρ gets up and stretches. | |
16 | [~sitting(ρ),~in(ρ,κ),empty(κ)] | |
14 | [actor(ρ)∧sitting(ρ)∧in(ρ,κ)] | |
15 | ρ gets up and stretches. | |
16 | [¬sitting(ρ)∧¬in(ρ,κ)∧empty(κ)] | |
17 | 17 | |
18 | end | |
18 | actor(Hastings). | |
19 | actor(Petersen). | |
20 | actor(Wembley). | |
21 | nearby(chair), empty(chair). | |
22 | nearby(recliner), empty(recliner). | |
23 | nearby(sofa), empty(sofa). | |
19 | 24 | |
20 | situations | |
21 | ||
22 | [ | |
23 | actor(Hastings), | |
24 | actor(Petersen), | |
25 | actor(Wembley), | |
26 | nearby(chair), empty(chair), | |
27 | nearby(recliner), empty(recliner), | |
28 | nearby(sofa), empty(sofa) | |
29 | ] | |
30 | ||
31 | end | |
25 | goal []. | |
26 | } |
0 | build/* |
0 | import sys | |
1 | ||
2 | for line in sys.stdin: | |
3 | l = line.strip() | |
4 | l = l.replace('_', ' ') | |
5 | if l: | |
6 | sys.stdout.write("{} ".format(l)) | |
7 | else: | |
8 | sys.stdout.write("\n\n") |
0 | #!/bin/sh | |
1 | ||
2 | THIS_SCRIPT=`realpath $0` | |
3 | cd `dirname $THIS_SCRIPT` | |
4 | mkdir -p build | |
5 | samovar scenes.samovar --seed=0 --min-events=40 > build/events.txt | |
6 | python formatter.py < build/events.txt > build/novel.md | |
7 | cat build/novel.md | |
8 | wc -w build/novel.md |
0 | // ----------------------------- MEANINGS ------------------------ | |
1 | ||
2 | // actor(?A) ?A is an actor in the scene. | |
3 | // shocked(?A) ?A is in mental state: shocked. | |
4 | // can_exit(?A) ?A is allowed to leave the scene. | |
5 | // item(?I) ?I is an item in the scene. | |
6 | // drink(?I) ?I is an item that can be drank. | |
7 | // holding(?A, ?I) ?A is an actor holding item ?I. | |
8 | // unheld(?I) ?I is an item that is not currently held. | |
9 | // prop(?P) ?P is a prop in the scene. | |
10 | // seat(?S) ?S is a prop that can be sat on. | |
11 | // sitting(?A) | |
12 | // sitting_on(?A,?S) | |
13 | // empty(?S) ?S is a seat that is not currently occupied. | |
14 | // fenestration(?P) ?P is a prop that can be looked out of to the outside. | |
15 | // near(?A) ?A is near a prop. | |
16 | // near_to(?A,?P) ?A is an actor who is near or next to prop ?P. | |
17 | // described(?P) ?P is a prop that has been described. | |
18 | ||
19 | // topic(?T) ?T is a topic of conversation. | |
20 | // has-news(?A,?T) ?A has news which is ?T | |
21 | ||
22 | // ----------------------------- RULES ------------------------ | |
23 | ||
24 | scenario Rules { | |
25 | [actor(?A),item(?I),unheld(?I)] ?A picked up the ?I. [holding(?A,?I),!unheld(?I)] | |
26 | [actor(?A),item(?I),holding(?A,?I)] ?A put down the ?I. [!holding(?A,?I),unheld(?I)] | |
27 | ||
28 | [actor(?A),item(?I),holding(?A,?I),drink(?I)] ?A took a sip of the ?I. [] | |
29 | ||
30 | [actor(?A),!sitting(?A)] ?A walked around the room. [] | |
31 | [actor(?A),!sitting(?A),seat(?S),empty(?S)] ?A sat down on the ?S. [sitting(?A),sitting_on(?A,?S),!empty(?S)] | |
32 | [actor(?A),sitting(?A),sitting_on(?A,?S)] ?A leaned back in the ?S. [] | |
33 | [actor(?A),sitting(?A),sitting_on(?A,?S)] ?A got up and stretched. [!sitting(?A),!sitting_on(?A,?S),empty(?S)] | |
34 | ||
35 | [actor(?A),can_exit(?A)] ?A left the room. [!actor(?A)] | |
36 | ||
37 | [actor(?A)] ?A coughed. [] | |
38 | [actor(?A)] ?A rubbed his chin. [] | |
39 | [actor(?A),shocked(?A)] ?A gasped. [] | |
40 | [actor(?A),shocked(?A)] ?A stared blankly off into space. [] | |
41 | [actor(?A),shocked(?A)] ?A winced. [] | |
42 | ||
43 | [actor(?A),actor(?B)] ?A looked at ?B. [] | |
44 | [actor(?A),actor(?B)] ?A nodded to ?B. [] | |
45 | ||
46 | [actor(?A),fenestration(?P)] ?A looked out the ?P. [] | |
47 | ||
48 | [actor(?A),!sitting(?A),prop(?P),!near(?A)] ?A walked over to the ?P. [near(?A),near_to(?A,?P)] | |
49 | [actor(?A),near_to(?A,?P)] ?A examined the ?P closely. [] | |
50 | [actor(?A),near_to(?A,?P)] ?A walked away from the ?P. [!near(?A),!near_to(?A,?P)] | |
51 | ||
52 | [prop(?P),!described(?P)] Nearby there was a ?P. [described(?P)] | |
53 | ||
54 | [actor(?A),has-news(?A,?T),!exclaimed-has-news(?A,?T)] ?A exclaimed, "I have news!" [exclaimed-has-news(?A,?T)] | |
55 | [actor(?A),exclaimed-has-news(?B,?T),!heard-news(?A,?T)] ?A asked, "What is it, ?B?" [been-asked-about-news(?B,?T)] | |
56 | [actor(?A),been-asked-about-news(?A,?T),actor(?B)] ?A told ?B of the ?T. [heard-news(?B,?T)] | |
57 | } | |
58 | ||
59 | // ----------------------------- SETTINGS ------------------------ | |
60 | ||
61 | scenario SittingRoom { | |
62 | prop(desk). | |
63 | prop(bookshelf). | |
64 | prop(window). fenestration(window). | |
65 | prop(grandfather_clock). | |
66 | prop(leather_chair). | |
67 | ||
68 | seat(leather_chair). | |
69 | empty(leather_chair). | |
70 | ||
71 | item(newspaper). | |
72 | item(whiskey). drink(whiskey). | |
73 | item(brandy). drink(brandy). | |
74 | item(pocket_watch). | |
75 | ||
76 | unheld(newspaper). | |
77 | unheld(whiskey). | |
78 | unheld(brandy). | |
79 | unheld(pcoket_watch). | |
80 | } | |
81 | ||
82 | scenario GardenShed { | |
83 | prop(workbench). | |
84 | prop(window). fenestration(window). | |
85 | prop(bags_of_potting_soil). | |
86 | ||
87 | seat(bags_of_potting_soil). | |
88 | empty(bags_of_potting_soil). | |
89 | ||
90 | item(oilcan). | |
91 | item(wrench). | |
92 | item(screwdriver). | |
93 | ||
94 | unheld(oilcan). | |
95 | unheld(wrench). | |
96 | unheld(screwdriver). | |
97 | } | |
98 | ||
99 | // ----------------------------- SCENES ------------------------ | |
100 | ||
101 | scenario Scene_1 { | |
102 | import SittingRoom. | |
103 | import Rules. | |
104 | ||
105 | actor(Pranehurst). | |
106 | actor(Scurthorpe). | |
107 | actor(Throgmorton). | |
108 | ||
109 | topic(impending_hurricane). | |
110 | has-news(Scurthorpe, impending_hurricane). | |
111 | ||
112 | shocked(Throgmorton). | |
113 | ||
114 | goal [heard-news(Throgmorton, impending_hurricane)]. | |
115 | } | |
116 | ||
117 | scenario Scene_2 { | |
118 | import GardenShed. | |
119 | import Rules. | |
120 | ||
121 | actor(Pranehurst). | |
122 | actor(Scurthorpe). | |
123 | actor(Throgmorton). | |
124 | ||
125 | shocked(Throgmorton). | |
126 | can_exit(Scurthorpe). | |
127 | ||
128 | goal []. | |
129 | } | |
130 | ||
131 | scenario Scene_3 { | |
132 | import SittingRoom. | |
133 | import Rules. | |
134 | ||
135 | actor(Pranehurst). | |
136 | actor(Scurthorpe). | |
137 | actor(Throgmorton). | |
138 | ||
139 | can_exit(Pranehurst). | |
140 | can_exit(Scurthorpe). | |
141 | can_exit(Throgmorton). | |
142 | ||
143 | goal []. | |
144 | } |
0 | rules | |
1 | ||
2 | [actor(ρ)] | |
3 | ρ scratches their(ρ) head. | |
4 | [] | |
5 | ||
6 | end | |
7 | ||
8 | functions | |
9 | ||
10 | their(Alice) → her | |
11 | their(Bob) → his | |
12 | ||
13 | end | |
14 | ||
15 | situations | |
16 | ||
17 | [ | |
18 | actor(Alice), | |
19 | actor(Bob) | |
20 | ] | |
21 | ||
22 | end |
0 | scenario HeadScratching { | |
1 | ||
2 | [actor(?Actor),possessive(?Actor,?their)] | |
3 | ?Actor scratches ?their head. | |
4 | [] | |
5 | ||
6 | actor(Alice). | |
7 | possessive(Alice, her). | |
8 | actor(Bob). | |
9 | possessive(Bob, his). | |
10 | ||
11 | goal []. | |
12 | } |
0 | 0 | # encoding: UTF-8 |
1 | 1 | |
2 | from copy import deepcopy | |
3 | from pprint import pprint | |
4 | import random | |
2 | from collections import namedtuple | |
5 | 3 | |
6 | 4 | |
7 | class AST(object): | |
8 | def __init__(self, **kwargs): | |
9 | self.attrs = kwargs | |
10 | ||
11 | def __repr__(self): | |
12 | return "%s(%s)" % ( | |
13 | self.__class__.__name__, | |
14 | ', '.join(['%s=%r' % (k, v) for k, v in self.attrs.iteritems()]) | |
15 | ) | |
16 | ||
17 | def __getattr__(self, name): | |
18 | if name in self.attrs: | |
19 | return self.attrs[name] | |
20 | raise AttributeError(name) | |
5 | # Python 2/3 | |
6 | try: | |
7 | unicode = unicode | |
8 | except NameError: | |
9 | unicode = str | |
21 | 10 | |
22 | 11 | |
23 | class World(AST): | |
24 | pass | |
12 | World = namedtuple('World', ['scenarios']) | |
13 | ||
14 | class Rule(namedtuple('Rule', ['pre', 'words', 'post'])): | |
15 | __slots__ = () | |
16 | ||
17 | def format(self, unifier): | |
18 | return join_sentence_parts([unicode(t.subst(unifier)) for t in self.words]) | |
19 | ||
20 | def to_json(self): | |
21 | return join_sentence_parts([unicode(t) for t in self.words]) | |
22 | ||
23 | Scenario = namedtuple('Scenario', ['name', 'propositions', 'rules', 'goal']) | |
24 | Cond = namedtuple('Cond', ['exprs', 'bindings']) | |
25 | Assert = namedtuple('Assert', ['term']) | |
26 | Retract = namedtuple('Retract', ['term']) | |
25 | 27 | |
26 | 28 | |
27 | class Rule(AST): | |
28 | def nu_format(self): | |
29 | return self.pre.format() + u" " + u' '.join([unicode(t) for t in self.terms]) + u" " + self.post.format() | |
30 | ||
31 | def format(self, unifier, functions): | |
32 | acc = u'' | |
33 | for t in self.terms: | |
34 | ||
35 | t = t.subst(unifier) | |
36 | ||
37 | # now resolve functions. NOTE: this is a terrible hacky just-for-now implementation. TODO: better it. | |
38 | for f in functions: | |
39 | if t == f.sign: | |
40 | t = f.result | |
41 | break | |
42 | ||
43 | s = unicode(t) | |
44 | if (acc == u'') or (s in (u'.', u',', u'!', u'"', u"'")): | |
45 | acc += s | |
29 | def join_sentence_parts(parts): | |
30 | acc = u'' | |
31 | quote_open = False | |
32 | for part in parts: | |
33 | last = '' if not acc else acc[-1] | |
34 | if last == '': | |
35 | acc += part | |
36 | elif last == '"' and quote_open: | |
37 | acc += part | |
38 | elif last == '"' and not quote_open: | |
39 | if part in (u'.', u',', u'!', u'?'): | |
40 | acc += part | |
46 | 41 | else: |
47 | acc += u' ' + s | |
48 | return acc | |
49 | ||
50 | ||
51 | class Function(AST): | |
52 | pass | |
53 | ||
54 | ||
55 | class Situation(AST): | |
56 | pass | |
57 | ||
58 | ||
59 | class Cond(AST): | |
60 | def __str__(self): | |
61 | return u'[%s]' % ','.join([unicode(e) for e in self.exprs]) | |
62 | ||
63 | def format(self): | |
64 | return u'[%s]' % ','.join([e.format() for e in self.exprs]) | |
65 | ||
66 | def eval(self, unifier, state): | |
67 | for expr in self.exprs: | |
68 | term = expr.term.subst(unifier) | |
69 | if isinstance(expr, Assert): | |
70 | if term not in state: | |
71 | return False | |
72 | if isinstance(expr, Retract): | |
73 | if term in state: | |
74 | return False | |
75 | return True | |
76 | ||
77 | ||
78 | class Assert(AST): | |
79 | def format(self): | |
80 | return u'%s' % self.term | |
81 | ||
82 | ||
83 | class Retract(AST): | |
84 | def format(self): | |
85 | return u'~%s' % self.term | |
42 | acc += ' ' + part | |
43 | elif last == ',' and part == u'"' and not quote_open: | |
44 | acc += ' ' + part | |
45 | elif last in (u'.', u',', u'!', u'?', u"'") and part == u'"' and not quote_open: | |
46 | acc += part | |
47 | elif part in (u'.', u',', u'!', u'?', u"'", u'"'): | |
48 | acc += part | |
49 | else: | |
50 | acc += u' ' + part | |
51 | if part == '"': | |
52 | quote_open = not quote_open | |
53 | return acc |
0 | 0 | # encoding: UTF-8 |
1 | 1 | |
2 | from itertools import permutations | |
3 | 2 | import random |
4 | import re | |
3 | import sys | |
5 | 4 | |
6 | 5 | from samovar.ast import Assert, Retract |
7 | from samovar.terms import Term | |
6 | from samovar.query import match_all | |
8 | 7 | |
9 | 8 | |
10 | def word_count(s): | |
11 | return len(re.split(r'\s+', s)) | |
9 | # Python 2/3 | |
10 | try: | |
11 | xrange = xrange | |
12 | except NameError: | |
13 | xrange = range | |
12 | 14 | |
13 | 15 | |
14 | def all_assignments(vars_, things): | |
15 | assignments = [] | |
16 | var_names = [v.name for v in vars_] | |
17 | for p in permutations(things, len(vars_)): | |
18 | assignments.append(dict(zip(var_names, p))) | |
19 | return assignments | |
16 | class Event(object): | |
17 | def __init__(self, rule, unifier): | |
18 | self.rule = rule | |
19 | self.unifier = unifier | |
20 | ||
21 | def to_json(self): | |
22 | u = dict([(unicode(k), unicode(v)) for k, v in self.unifier.items()]) | |
23 | return [self.rule.to_json(), u] | |
24 | ||
25 | def __str__(self): | |
26 | return self.rule.format(self.unifier) | |
20 | 27 | |
21 | 28 | |
22 | 29 | class Generator(object): |
23 | def __init__(self, world, debug=False): | |
30 | def __init__(self, world, scenario, debug=False, verbose=False): | |
24 | 31 | self.world = world |
25 | 32 | self.debug = debug |
33 | self.verbose = verbose | |
34 | self.scenario = scenario | |
35 | self.reset_state() | |
36 | ||
37 | def reset_state(self): | |
26 | 38 | self.state = set() # set of things currently true about the world |
27 | self.things = set() | |
28 | for e in self.world.situations[0].cond.exprs: | |
29 | if isinstance(e, Assert): | |
30 | self.state.add(e.term) | |
31 | atoms = set() | |
32 | e.term.collect_atoms(atoms) | |
33 | self.things |= atoms | |
39 | for term in self.scenario.propositions: | |
40 | self.state.add(term) | |
34 | 41 | |
35 | def generate_events(self, count): | |
36 | if self.debug: | |
37 | self.debug_state() | |
38 | moves = [] | |
39 | for i in xrange(0, count): | |
40 | moves.append(self.generate_move()) | |
41 | return moves | |
42 | def generate_events(self, count, max_count, lengthen_factor): | |
43 | acceptable = False | |
44 | while not acceptable: | |
45 | if self.verbose: | |
46 | sys.stderr.write("Generating {} events\n".format(count)) | |
47 | self.reset_state() | |
48 | if self.debug: | |
49 | self.debug_state("Initial") | |
50 | events = [] | |
51 | for i in xrange(0, count): | |
52 | event = self.generate_event() | |
53 | if event is None: | |
54 | break | |
55 | events.append(event) | |
56 | if self.debug: | |
57 | self.debug_state("Final") | |
58 | acceptable = self.events_meet_goal(events) | |
59 | if not acceptable: | |
60 | count = int(float(count) * lengthen_factor) | |
61 | if count > max_count: | |
62 | raise ValueError("{}: count exceeds maximum".format(self.scenario.name)) | |
63 | return events | |
42 | 64 | |
43 | def generate_words(self, target): | |
44 | if self.debug: | |
45 | self.debug_state() | |
46 | moves = [] | |
47 | count = 0 | |
48 | while count < target: | |
49 | move = self.generate_move() | |
50 | count += word_count(move) | |
51 | moves.append(move) | |
52 | return moves | |
65 | def events_meet_goal(self, moves): | |
66 | matches = match_all(self.state, self.scenario.goal.exprs, self.scenario.goal.bindings) | |
67 | return len(matches) > 0 | |
53 | 68 | |
54 | def generate_move(self): | |
69 | def generate_event(self): | |
55 | 70 | candidates = self.get_candidate_rules() |
71 | if not candidates: | |
72 | return None | |
56 | 73 | rule, unifier = random.choice(candidates) |
57 | move = rule.format(unifier, self.world.functions) | |
58 | 74 | self.update_state(unifier, rule) |
59 | return move | |
75 | return Event(rule, unifier) | |
60 | 76 | |
61 | 77 | def get_candidate_rules(self): |
62 | 78 | candidates = [] |
63 | for rule in self.world.rules: | |
64 | vars_ = set() | |
65 | for expr in rule.pre.exprs: | |
66 | expr.term.collect_variables(vars_) | |
79 | for rule in self.scenario.rules: | |
80 | for unifier in match_all(self.state, rule.pre.exprs, rule.pre.bindings): | |
81 | candidates.append((rule, unifier)) | |
67 | 82 | |
68 | for a in all_assignments(vars_, self.things): | |
69 | result = rule.pre.eval(a, self.state) | |
70 | if self.debug: | |
71 | print rule.nu_format(), "WITH", a, "==>", result | |
72 | if result: | |
73 | candidates.append((rule, a)) | |
74 | ||
75 | if self.debug: | |
76 | print "Candidate rules:" | |
83 | if self.debug and False: # FIXME | |
84 | sys.stderr.write("Candidate rules:") | |
77 | 85 | for rule, unifiers in candidates: |
78 | print rule.nu_format() | |
79 | print "->", unifiers | |
80 | ||
86 | sys.stderr.write(rule.format()) | |
87 | sys.stderr.write("->", unifiers) | |
88 | sys.stderr.write("") | |
81 | 89 | |
82 | 90 | return candidates |
83 | 91 | |
88 | 96 | self.state.add(term) |
89 | 97 | elif isinstance(expr, Retract): |
90 | 98 | self.state.remove(term) |
91 | if self.debug: | |
99 | if self.debug and False: # FIXME | |
92 | 100 | self.debug_state() |
93 | 101 | |
94 | def debug_state(self): | |
95 | print "Things now:" | |
96 | for term in self.things: | |
97 | print u" %s" % term | |
98 | print "State now:" | |
99 | for term in self.state: | |
100 | print u" %s" % term | |
101 | ||
102 | def debug_state(self, label): | |
103 | sys.stderr.write(":::: {} State [\n".format(label)) | |
104 | for term in sorted(self.state): | |
105 | sys.stderr.write(":::: {}\n".format(term)) | |
106 | sys.stderr.write(":::: ]\n") |
0 | 0 | # encoding: UTF-8 |
1 | 1 | |
2 | from samovar.ast import World, Rule, Function, Situation, Cond, Assert, Retract | |
2 | from samovar.ast import World, Scenario, Rule, Cond, Assert, Retract | |
3 | 3 | from samovar.terms import Term, Var |
4 | 4 | from samovar.scanner import Scanner |
5 | 5 | |
6 | 6 | |
7 | # World ::= {Rules | Functions | Situations}. | |
8 | # Rules ::= "rules" {Rule} "end". | |
9 | # Functions ::= "functions" {Function} "end". | |
10 | # Situations ::= "situations" {Situation} "end". | |
11 | # Rule ::= Cond {Term | Punct} Cond. | |
12 | # Function ::= Term "→" Term. | |
13 | # Situation ::= Cond. | |
14 | # Cond ::= "[" Expr {"," Expr} "]". | |
15 | # Expr ::= Term | "~" Term. | |
16 | # Term ::= Var | Word ["(" Term {"," Term} ")"]. | |
17 | # Var ::= <<one of: αβγδεζθικλμνξοπρστυφχψω>> | |
18 | # Atom ::= <<A-Za-z possibly with punctuation on either end>> | |
7 | # World ::= {Scenario}. | |
8 | # Scenario ::= "scenario" Atom "{" {Import | Proposition | Rule | Goal} ["." | ","] "}". | |
9 | # Import ::= "import" Atom. | |
10 | # Goal ::= "goal" Cond. | |
11 | # Proposition ::= Term. | |
12 | # Rule ::= Cond {Var | Atom | Punct} Cond. | |
13 | # Cond ::= "[" Expr {"," Expr} ["where" {Var "=" Term}"]". | |
14 | # Expr ::= Term | NotSym Term. | |
15 | # Term ::= Var | Atom ["(" Term {AndSym Term} ")"]. | |
16 | # Var ::= Qmark | Greek. | |
17 | # Qmark ::= '?' Atom. | |
18 | # Greek ::= <<one of: αβγδεζθικλμνξοπρστυφχψω>>. | |
19 | # Atom ::= <<A-Za-z_>> <<A-Za-z0-9_-'>>*. | |
20 | # Punct ::= <<"',.;:?!>>. | |
21 | # NotSym ::= '~' | '¬'. | |
22 | # AndSym ::= ',' | '∧'. | |
19 | 23 | |
20 | 24 | |
21 | 25 | class Parser(object): |
22 | 26 | def __init__(self, text): |
23 | 27 | self.scanner = Scanner(text) |
28 | self.scenario_map = {} | |
24 | 29 | |
25 | 30 | def world(self): |
31 | scenarios = [] | |
32 | while self.scanner.on('scenario'): | |
33 | scenario = self.scenario() | |
34 | self.scenario_map[scenario.name] = scenario | |
35 | scenarios.append(scenario) | |
36 | return World(scenarios=scenarios) | |
37 | ||
38 | def scenario(self): | |
39 | propositions = [] | |
26 | 40 | rules = [] |
27 | functions = [] | |
28 | situations = [] | |
29 | while self.scanner.on('rules', 'functions', 'situations'): | |
30 | if self.scanner.on('rules'): | |
31 | rules.extend(self._section('rules', self.rule)) | |
32 | if self.scanner.on('functions'): | |
33 | functions.extend(self._section('functions', self.function)) | |
34 | if self.scanner.on('situations'): | |
35 | situations.extend(self._section('situations', self.situation)) | |
36 | return World(rules=rules, functions=functions, situations=situations) | |
41 | goal = None | |
42 | self.scanner.expect('scenario') | |
43 | self.scanner.check_type('word') | |
44 | name = self.scanner.token | |
45 | self.scanner.scan() | |
46 | self.scanner.expect('{') | |
47 | while not self.scanner.on('}'): | |
48 | if self.scanner.consume('import'): | |
49 | self.scanner.check_type('word') | |
50 | from_name = self.scanner.token | |
51 | self.scanner.scan() | |
52 | from_scenario = self.scenario_map[from_name] | |
53 | rules.extend(from_scenario.rules) | |
54 | propositions.extend(from_scenario.propositions) | |
55 | elif self.scanner.consume('goal'): | |
56 | assert goal is None | |
57 | goal = self.cond() | |
58 | elif self.scanner.on('['): | |
59 | rules.append(self.rule()) | |
60 | else: | |
61 | propositions.append(self.proposition()) | |
62 | self.scanner.consume('.') | |
63 | self.scanner.consume(',') | |
64 | self.scanner.expect('}') | |
65 | return Scenario(name=name, propositions=propositions, rules=rules, goal=goal) | |
37 | 66 | |
38 | def _section(self, heading, method): | |
39 | items = [] | |
40 | self.scanner.expect(heading) | |
41 | while not self.scanner.on('end'): | |
42 | items.append(method()) | |
43 | self.scanner.expect('end') | |
44 | return items | |
67 | def proposition(self): | |
68 | return self.term() | |
45 | 69 | |
46 | 70 | def rule(self): |
47 | terms = [] | |
71 | words = [] | |
48 | 72 | pre = self.cond() |
49 | 73 | while not self.scanner.on('['): |
50 | terms.append(self.term()) | |
74 | words.append(self.word()) | |
51 | 75 | post = self.cond() |
52 | return Rule(pre=pre, terms=terms, post=post) | |
53 | ||
54 | def function(self): | |
55 | sign = self.term() | |
56 | self.scanner.expect(u'→') | |
57 | result = self.term() | |
58 | return Function(sign=sign, result=result) | |
59 | ||
60 | def situation(self): | |
61 | cond = self.cond() | |
62 | return Situation(cond=cond) | |
76 | return Rule(pre=pre, words=words, post=post) | |
63 | 77 | |
64 | 78 | def cond(self): |
65 | 79 | exprs = [] |
80 | bindings = {} | |
66 | 81 | self.scanner.expect('[') |
67 | if not self.scanner.on(']'): | |
82 | if not self.scanner.on(']') and not self.scanner.on('where'): | |
68 | 83 | exprs.append(self.expr()) |
69 | while self.scanner.consume(','): | |
84 | while self.scanner.consume(',', u'∧'): | |
70 | 85 | exprs.append(self.expr()) |
86 | if self.scanner.consume('where'): | |
87 | while not self.scanner.on(']'): | |
88 | v = self.var() | |
89 | self.scanner.expect('=') | |
90 | t = self.term() | |
91 | bindings[v.name] = t | |
71 | 92 | self.scanner.expect(']') |
72 | return Cond(exprs=exprs) | |
93 | return Cond(exprs=exprs, bindings=bindings) | |
73 | 94 | |
74 | 95 | def expr(self): |
75 | if self.scanner.consume('~'): | |
96 | if self.scanner.consume('~', u'¬', '!'): | |
76 | 97 | return Retract(term=self.term()) |
77 | 98 | else: |
78 | 99 | return Assert(term=self.term()) |
80 | 101 | def term(self): |
81 | 102 | if self.scanner.on_type('variable'): |
82 | 103 | return self.var() |
83 | self.scanner.check_type('word', 'punct') | |
104 | self.scanner.check_type('word') | |
84 | 105 | constructor = self.scanner.token |
85 | 106 | self.scanner.scan() |
86 | 107 | subterms = [] |
89 | 110 | while self.scanner.consume(','): |
90 | 111 | subterms.append(self.term()) |
91 | 112 | self.scanner.expect(')') |
92 | return Term(constructor, subterms=subterms) | |
113 | return Term(constructor, *subterms) | |
114 | ||
115 | def word(self): | |
116 | if self.scanner.on_type('variable'): | |
117 | return self.var() | |
118 | self.scanner.check_type('word', 'punct', 'operator') | |
119 | constructor = self.scanner.token | |
120 | self.scanner.scan() | |
121 | return Term(constructor) | |
93 | 122 | |
94 | 123 | def var(self): |
95 | 124 | self.scanner.check_type('variable') |
96 | 125 | name = self.scanner.token |
97 | 126 | self.scanner.scan() |
98 | return Var(name) | |
127 | v = Var(name) | |
128 | return v |
0 | from samovar.ast import Assert, Retract | |
1 | ||
2 | ||
3 | def match_all(database, patterns, env): | |
4 | """Find all matches for the given patterns in the database, and return a list of unifiers. | |
5 | ||
6 | `database` is an iterable of `Term`s. | |
7 | `patterns` is a list of `Assert`s and `Retract`s. | |
8 | `env` is a dict mapping `Var` names to `Term`s (a previous unifier.) | |
9 | ||
10 | """ | |
11 | if not patterns: | |
12 | return [env] | |
13 | envs = [] | |
14 | pattern = patterns[0] | |
15 | ||
16 | if isinstance(pattern, Assert): | |
17 | for proposition in database: | |
18 | try: | |
19 | unifier = pattern.term.match(proposition, env, unique_binding=True) | |
20 | except ValueError: | |
21 | continue | |
22 | envs.extend(match_all(database, patterns[1:], unifier)) | |
23 | ||
24 | elif isinstance(pattern, Retract): | |
25 | # to test a negative match, we require first that there are | |
26 | # no free variables in our pattern. | |
27 | ||
28 | expanded_pattern = pattern.term.subst(env) | |
29 | free_vars = set() | |
30 | expanded_pattern.collect_variables(free_vars) | |
31 | if free_vars: | |
32 | # TODO: better exception than this | |
33 | raise NotImplementedError | |
34 | ||
35 | # now we simply check if the term exists in the database. | |
36 | # if it does not, we recurse down to the next clause in the pattern. | |
37 | ||
38 | if not (expanded_pattern in database): | |
39 | envs.extend(match_all(database, patterns[1:], env)) | |
40 | ||
41 | else: | |
42 | raise NotImplementedError | |
43 | ||
44 | return envs |
0 | 0 | # encoding: UTF-8 |
1 | 1 | |
2 | ||
3 | # Python 2/3 | |
4 | try: | |
5 | unicode = unicode | |
6 | except NameError: | |
7 | unicode = str | |
8 | ||
9 | ||
2 | 10 | import re |
11 | ||
12 | ||
13 | GREEK = [ | |
14 | (u'?alpha', u'α'), | |
15 | (u'?beta', u'β'), | |
16 | (u'?gamma', u'γ'), | |
17 | (u'?delta', u'δ'), | |
18 | (u'?epsilon', u'ε'), | |
19 | (u'?zeta', u'ζ'), | |
20 | (u'?theta', u'θ'), | |
21 | (u'?iota', u'ι'), | |
22 | (u'?kappa', u'κ'), | |
23 | (u'?lambda', u'λ'), | |
24 | (u'?mu', u'μ'), | |
25 | (u'?nu', u'ν'), | |
26 | (u'?xi', u'ξ'), | |
27 | (u'?omicron', u'ο'), | |
28 | (u'?pi', u'π'), | |
29 | (u'?rho', u'ρ'), | |
30 | (u'?sigma', u'σ'), | |
31 | (u'?tau', u'τ'), | |
32 | (u'?upsilon', u'υ'), | |
33 | (u'?phi', u'φ'), | |
34 | (u'?chi', u'χ'), | |
35 | (u'?psi', u'ψ'), | |
36 | (u'?omega', u'ω'), | |
37 | ] | |
3 | 38 | |
4 | 39 | |
5 | 40 | class Scanner(object): |
33 | 68 | self.token = None |
34 | 69 | self.type = 'EOF' |
35 | 70 | return |
36 | if self.scan_pattern(ur'\~|→', 'operator'): | |
71 | if self.scan_pattern(u'\\~|→|=|¬|∧|∨', 'operator'): | |
37 | 72 | return |
38 | if self.scan_pattern(r'\,|\.|\?|\!|\"' + r"|\'", 'punct'): | |
73 | # TODO: not sure about the ? overloading (here and in punct). should be okay though? | |
74 | if self.scan_pattern(r'\?[a-zA-Z_]+', 'variable'): | |
75 | return | |
76 | if self.scan_pattern(r'\,|\.|\;|\:|\?|\!|\"', 'punct'): | |
39 | 77 | return |
40 | 78 | if self.scan_pattern(r'\(|\)|\{|\}|\[|\]', 'bracket'): |
41 | 79 | return |
42 | if self.scan_pattern(r'[a-zA-Z_]+', 'word'): | |
80 | if self.scan_pattern(r"[a-zA-Z_]['a-zA-Z0-9_-]*", 'word'): | |
43 | 81 | return |
44 | if self.scan_pattern(ur'[αβγδεζθικλμνξοπρστυφχψω]', 'variable'): | |
82 | if self.scan_pattern(u'[αβγδεζθικλμνξοπρστυφχψω]', 'variable'): | |
83 | for varname, letter in GREEK: | |
84 | if letter == self.token: | |
85 | self.token = varname | |
86 | break | |
87 | assert self.token.startswith('?'), repr(self.token) | |
45 | 88 | return |
46 | 89 | if self.scan_pattern(r'.', 'unknown character'): |
47 | 90 | return |
66 | 109 | raise SyntaxError(u"Expected %s, but found %s ('%s') (near '%s')" % |
67 | 110 | (types, self.type, self.token, self.near_text())) |
68 | 111 | |
69 | def consume(self, token): | |
70 | if self.token == token: | |
112 | def consume(self, *tokens): | |
113 | if self.token in tokens: | |
71 | 114 | self.scan() |
72 | 115 | return True |
73 | 116 | else: |
0 | 0 | # encoding: UTF-8 |
1 | 1 | |
2 | 2 | |
3 | # Python 2/3 | |
4 | try: | |
5 | unicode = unicode | |
6 | except NameError: | |
7 | unicode = str | |
8 | ||
9 | ||
3 | 10 | class AbstractTerm(object): |
4 | ||
5 | def __ne__(self, other): | |
6 | return not self.__eq__(other) | |
7 | ||
8 | def __hash__(self): | |
9 | return hash(unicode(self)) | |
10 | ||
11 | def match_many(self, terms): | |
12 | successes = [] | |
13 | for term in terms: | |
14 | unifier = {} | |
15 | try: | |
16 | self.match(term, unifier) | |
17 | successes.append((term, unifier)) | |
18 | except ValueError as e: | |
19 | pass | |
20 | return successes | |
11 | pass | |
21 | 12 | |
22 | 13 | |
23 | 14 | class Term(AbstractTerm): |
24 | def __init__(self, constructor, subterms=None): | |
25 | if subterms is None: | |
26 | subterms = [] | |
27 | self.constructor = constructor | |
28 | self.subterms = subterms | |
15 | def __init__(self, constructor, *subterms): | |
16 | self.t = tuple([constructor] + list(subterms)) | |
17 | ||
18 | @property | |
19 | def constructor(self): | |
20 | return self.t[0] | |
21 | ||
22 | @property | |
23 | def subterms(self): | |
24 | return self.t[1:] | |
29 | 25 | |
30 | 26 | def __str__(self): |
31 | 27 | if len(self.subterms) == 0: |
34 | 30 | |
35 | 31 | def __repr__(self): |
36 | 32 | if self.subterms: |
37 | return "%s(%r, subterms=%r)" % ( | |
33 | return "%s(%r, *%r)" % ( | |
38 | 34 | self.__class__.__name__, self.constructor, self.subterms |
39 | 35 | ) |
40 | 36 | else: |
43 | 39 | ) |
44 | 40 | |
45 | 41 | def __eq__(self, other): |
46 | if not isinstance(other, Term): | |
47 | return False | |
48 | if self.constructor != other.constructor: | |
49 | return False | |
50 | if len(self.subterms) != len(other.subterms): | |
51 | return False | |
52 | for (st1, st2) in zip(self.subterms, other.subterms): | |
53 | if st1 != st2: | |
54 | return False | |
55 | return True | |
42 | return isinstance(other, Term) and self.t == other.t | |
43 | ||
44 | def __hash__(self): | |
45 | return hash(self.t) | |
56 | 46 | |
57 | 47 | def is_atom(self): |
58 | 48 | return len(self.subterms) == 0 |
63 | 53 | return False |
64 | 54 | return True |
65 | 55 | |
66 | def contains(self, other): | |
67 | if self == other: | |
68 | return True | |
69 | for st in self.subterms: | |
70 | if st.contains(other): | |
71 | return True | |
72 | return False | |
73 | ||
74 | def replace(self, old, new): | |
75 | if self == old: | |
76 | return new | |
77 | else: | |
78 | return Term(self.constructor, subterms=[subterm.replace(old, new) for subterm in self.subterms]) | |
79 | ||
80 | def match(self, term, unifier): | |
56 | def match(self, term, env, **kwargs): | |
81 | 57 | if self.constructor != term.constructor: |
82 | 58 | raise ValueError("`%s` != `%s`" % (self.constructor, term.constructor)) |
83 | 59 | if len(self.subterms) != len(term.subterms): |
84 | 60 | raise ValueError("`%s` != `%s`" % (len(self.subterms), len(term.subterms))) |
85 | 61 | for (subpat, subterm) in zip(self.subterms, term.subterms): |
86 | subpat.match(subterm, unifier) | |
62 | env = subpat.match(subterm, env, **kwargs) | |
63 | return env | |
87 | 64 | |
88 | def subst(self, unifier): | |
89 | return Term(self.constructor, subterms=[subterm.subst(unifier) for subterm in self.subterms]) | |
65 | def subst(self, env): | |
66 | return Term(self.constructor, *[subterm.subst(env) for subterm in self.subterms]) | |
90 | 67 | |
91 | 68 | def collect_atoms(self, atoms): |
92 | 69 | if self.is_atom(): |
111 | 88 | return "%s(%r)" % (self.__class__.__name__, self.name) |
112 | 89 | |
113 | 90 | def __eq__(self, other): |
114 | if not isinstance(other, Var): | |
115 | return False | |
116 | return self.name == other.name | |
91 | return isinstance(other, Var) and self.name == other.name | |
92 | ||
93 | def __hash__(self): | |
94 | return hash(self.name) | |
117 | 95 | |
118 | 96 | def is_atom(self): |
119 | 97 | return False |
121 | 99 | def is_ground(term): |
122 | 100 | return False |
123 | 101 | |
124 | def match(self, term, unifier): | |
125 | if self.name in unifier: | |
126 | unifier[self.name].match(term, unifier) | |
102 | def match(self, term, env, **kwargs): | |
103 | if self.name in env: | |
104 | bound_to = env[self.name] | |
105 | return bound_to.match(term, env, **kwargs) | |
127 | 106 | else: |
128 | unifier[self.name] = term | |
107 | if kwargs.get('unique_binding'): | |
108 | if term in env.values(): | |
109 | raise ValueError("Not unique") | |
110 | return dict(list(env.items()) + [(self.name, term)]) | |
129 | 111 | |
130 | def subst(self, unifier): | |
131 | return unifier[self.name] | |
112 | def subst(self, env): | |
113 | return env[self.name] | |
132 | 114 | |
133 | 115 | def collect_atoms(self, atoms): |
134 | 116 | pass |
0 | import unittest | |
1 | from unittest import TestCase | |
2 | ||
3 | from samovar.ast import Assert, Retract, join_sentence_parts | |
4 | from samovar.terms import Term, Var | |
5 | from samovar.query import match_all | |
6 | ||
7 | ||
8 | def t(s, *args): | |
9 | if s.startswith('?'): | |
10 | return Var(s) | |
11 | else: | |
12 | return Term(s, *args) | |
13 | ||
14 | ||
15 | def a(t): | |
16 | return Assert(term=t) | |
17 | ||
18 | ||
19 | def r(t): | |
20 | return Retract(term=t) | |
21 | ||
22 | ||
23 | class TermTestCase(TestCase): | |
24 | def test_term_basic_properties(self): | |
25 | t1 = Term('alice') | |
26 | t2 = Term('actor', t1) | |
27 | v1 = Var('?A') | |
28 | t3 = Term('actor', v1) | |
29 | ||
30 | self.assertTrue(t1.is_atom()) | |
31 | self.assertFalse(t2.is_atom()) | |
32 | self.assertFalse(v1.is_atom()) | |
33 | self.assertFalse(t3.is_atom()) | |
34 | ||
35 | self.assertTrue(t1.is_ground()) | |
36 | self.assertTrue(t2.is_ground()) | |
37 | self.assertFalse(v1.is_ground()) | |
38 | self.assertFalse(t3.is_ground()) | |
39 | ||
40 | self.assertEqual(t2, Term('actor', Term('alice'))) | |
41 | ||
42 | def test_term_match_ground(self): | |
43 | t1 = Term('actor', Term('alice')) | |
44 | p1 = Term('actor', Term('alice')) | |
45 | u = p1.match(t1, {}) | |
46 | self.assertEqual(u, {}) | |
47 | ||
48 | def test_term_no_match_ground(self): | |
49 | t1 = Term('actor', Term('alice')) | |
50 | p1 = Term('actor', Term('bob')) | |
51 | with self.assertRaises(ValueError): | |
52 | 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 | with self.assertRaises(ValueError): | |
73 | p1.match(t1, u) | |
74 | self.assertEqual(u, {u'?A': Term('bob')}) | |
75 | ||
76 | def test_term_subst(self): | |
77 | t = Term('actor', Var('?A')) | |
78 | r = t.subst({u'?A': Term('alice')}) | |
79 | self.assertEqual(r, Term('actor', Term('alice'))) | |
80 | ||
81 | ||
82 | class RenderTestCase(TestCase): | |
83 | def test_join_sentence_parts_1(self): | |
84 | self.assertEqual( | |
85 | join_sentence_parts(['"', "Hello", ",", '"', "said", "the", "mouse", "."]), | |
86 | '"Hello," said the mouse.' | |
87 | ) | |
88 | ||
89 | def test_join_sentence_parts_2(self): | |
90 | self.assertEqual( | |
91 | join_sentence_parts(["The", "mouse", "asked", ",", '"', "What", "is", "it", "?", '"']), | |
92 | 'The mouse asked, "What is it?"' | |
93 | ) | |
94 | ||
95 | def test_join_sentence_parts_3(self): | |
96 | self.assertEqual( | |
97 | join_sentence_parts(["It", "was", "very", ",", "very", "dark", '.']), | |
98 | 'It was very, very dark.' | |
99 | ) | |
100 | ||
101 | ||
102 | class DatabaseTestCase(unittest.TestCase): | |
103 | ||
104 | def setUp(self): | |
105 | self.database = [ | |
106 | t('actor', t('alice')), | |
107 | t('actor', t('bob')), | |
108 | ||
109 | t('drink', t('gin')), | |
110 | ||
111 | t('weapon', t('revolver')), | |
112 | t('weapon', t('knife')), | |
113 | t('weapon', t('club')), | |
114 | ||
115 | t('holding', t('bob'), t('revolver')), | |
116 | t('holding', t('alice'), t('gin')), | |
117 | t('holding', t('alice'), t('knife')), | |
118 | ] | |
119 | ||
120 | ||
121 | class TestMatchAll(DatabaseTestCase): | |
122 | ||
123 | def assertMatchAll(self, query, result): | |
124 | self.assertEqual(match_all(self.database, query, {}), result) | |
125 | ||
126 | def test_match_all(self): | |
127 | # Find all actors who are Cody. Since there is no such actor, this will return no matches. | |
128 | self.assertMatchAll( | |
129 | [a(t('actor', t('cody')))], | |
130 | [] | |
131 | ) | |
132 | # Find all actors who are Alice. This will return one match, but no bindings. | |
133 | self.assertMatchAll( | |
134 | [a(t('actor', t('alice')))], | |
135 | [{}] | |
136 | ) | |
137 | # Find all drinks. This will return one match, with ?D bound to the result. | |
138 | self.assertMatchAll( | |
139 | [a(t("drink", t("?D")))], | |
140 | [{'?D': Term('gin')}] # there was a match, in which ?D was bound | |
141 | ) | |
142 | # Find all actors. | |
143 | self.assertMatchAll( | |
144 | [a(t('actor', t('?C')))], | |
145 | [{'?C': Term('alice')}, {'?C': Term('bob')}] | |
146 | ) | |
147 | # Find all actors who are holding the revolver. | |
148 | self.assertMatchAll( | |
149 | [a(t('actor', t('?C'))), a(t('holding', t('?C'), t('revolver')))], | |
150 | [{'?C': t('bob')}] | |
151 | ) | |
152 | # Find all actors who are holding a weapon. | |
153 | self.assertMatchAll( | |
154 | [a(t('actor', t('?C'))), a(t('weapon', t('?W'))), a(t('holding', t('?C'), t('?W')))], | |
155 | [{'?W': t('knife'), '?C': t('alice')}, {'?W': t('revolver'), '?C': t('bob')}] | |
156 | ) | |
157 | ||
158 | def test_match_all_with_unique_binding(self): | |
159 | # Find all pairs of actors. Because the actors must be different, there are only 2 matches. | |
160 | self.assertMatchAll( | |
161 | [a(t('actor', t('?A'))), a(t('actor', t('?B')))], | |
162 | [{'?A': Term('alice'), '?B': Term('bob')}, {'?A': Term('bob'), '?B': Term('alice')}] | |
163 | ) | |
164 | # Find all pairs of drinks. Since there is only one, and we can't return (gin,gin), | |
165 | # there will be no matches. | |
166 | self.assertMatchAll( | |
167 | [a(t('drink', t('?A'))), a(t('drink', t('?B')))], | |
168 | [] | |
169 | ) | |
170 | ||
171 | def test_match_all_with_negation(self): | |
172 | # Find all actors who are not holding the revolver. | |
173 | self.assertMatchAll( | |
174 | [a(t('actor', t('?C'))), r(t('holding', t('?C'), t('revolver')))], | |
175 | [{'?C': t('alice')}] | |
176 | ) | |
177 | # Find all actors who are not holding a weapon. Or rather, all pairs | |
178 | # of (actor, weapon) where the actor is not holding that weapon. | |
179 | self.assertMatchAll( | |
180 | [a(t('actor', t('?C'))), a(t('weapon', t('?W'))), r(t('holding', t('?C'), t('?W')))], | |
181 | [ | |
182 | {'?W': t('revolver'), '?C': t('alice')}, | |
183 | {'?W': t('club'), '?C': t('alice')}, | |
184 | {'?W': t('knife'), '?C': t('bob')}, | |
185 | {'?W': t('club'), '?C': t('bob')}, | |
186 | ] | |
187 | ) | |
188 | # Note that we can't say "Find all actors who aren't Alice". | |
189 | # We can say this: | |
190 | self.assertMatchAll( | |
191 | [a(t('actor', t('?C'))), r(t('actor', t('alice')))], | |
192 | [] | |
193 | ) | |
194 | # ... but what this is saying is "Find all actors if Alice doesn't exist." | |
195 | ||
196 | # For a one-off case, we can do something like this: | |
197 | self.database.append(t('is_alice', t('alice'))) | |
198 | self.assertMatchAll( | |
199 | [a(t('actor', t('?C'))), r(t('is_alice', t('?C')))], | |
200 | [{'?C': t('bob')}] | |
201 | ) | |
202 | ||
203 | # For the general case, we'll need to think about equality tests. | |
204 | ||
205 | # Note also that we can't search on negative clauses with free variables: | |
206 | with self.assertRaises(KeyError): | |
207 | match_all(self.database, [a(t('actor', t('?C'))), r(t('weapon', t('?W')))], {}) | |
208 | ||
209 | ||
210 | if __name__ == '__main__': | |
211 | unittest.main() |