git @ Cat's Eye Technologies Samovar / e71bc7c
Merge pull request #9 from catseye/develop-0.2 Develop 0.2 Chris Pressey authored 6 years ago GitHub committed 6 years ago
20 changed file(s) with 1290 addition(s) and 385 deletion(s). Raw diff Collapse all Expand all
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.
00 Samovar
11 =======
22
3 *Version 0.1. Subject to change in backwards-incompatible ways.*
3 *Version 0.2. Subject to change in backwards-incompatible ways.*
44
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.
67
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:
99
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
1237 sugar.
1338
1439 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
1641 proving that the action satisfies the conditions, though, we simply assume it
1742 does, and use the conditions to chain actions together in a sensible order.
1843
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.
2147
22 [A] X [B]
48 [Hoare logic]: https://en.wikipedia.org/wiki/Hoare_logic
2349
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
2651
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
33 """\
44 samovar {option} input.samovar
55
6 Driver script for Samovr assertion-retraction engine.
6 Driver script for Samovar assertion-retraction engine.
77 """
88
99 from os.path import realpath, dirname, join
1111
1212 sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
1313
14 from argparse import ArgumentParser
1415 import codecs
15 from optparse import OptionParser
16 import json
17 import random
1618
1719 from samovar.parser import Parser
1820 from samovar.generator import Generator
1921
2022
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)
2231
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",
2846 action="store_true",
2947 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)
3777
3878 text = ''
39 for arg in args:
79 for arg in options.input_files:
4080 with codecs.open(arg, 'r', encoding='UTF-8') as f:
4181 text += f.read()
4282
4383 p = Parser(text)
4484 ast = p.world()
4585 if options.dump_ast:
46 print ast
86 print(ast)
4787 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 {
11
2 [actor(ρ),~sitting(ρ)]
3 ρ walks around the room.
4 []
2 [actor(ρ)∧¬sitting(ρ)]
3 ρ walks around the room.
4 []
55
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(κ)]
99
10 [actor(ρ),sitting(ρ),in(ρ,κ)]
11 ρ leans back in the κ.
12 []
10 [actor(ρ)∧sitting(ρ)∧in(ρ,κ)]
11 ρ leans back in the κ.
12 []
1313
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(κ)]
1717
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).
1924
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 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
-23
eg/functions.samovar less more
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
-11
eg/idle.samovar less more
0 rules
1
2 [actor(ρ)]
3 ρ rubs his chin.
4 []
5
6 [actor(ρ)]
7 ρ yawns.
8 []
9
10 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 }
00 # encoding: UTF-8
11
2 from copy import deepcopy
3 from pprint import pprint
4 import random
2 from collections import namedtuple
53
64
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
2110
2211
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'])
2527
2628
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
4641 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
00 # encoding: UTF-8
11
2 from itertools import permutations
32 import random
4 import re
3 import sys
54
65 from samovar.ast import Assert, Retract
7 from samovar.terms import Term
6 from samovar.query import match_all
87
98
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
1214
1315
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)
2027
2128
2229 class Generator(object):
23 def __init__(self, world, debug=False):
30 def __init__(self, world, scenario, debug=False, verbose=False):
2431 self.world = world
2532 self.debug = debug
33 self.verbose = verbose
34 self.scenario = scenario
35 self.reset_state()
36
37 def reset_state(self):
2638 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)
3441
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
4264
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
5368
54 def generate_move(self):
69 def generate_event(self):
5570 candidates = self.get_candidate_rules()
71 if not candidates:
72 return None
5673 rule, unifier = random.choice(candidates)
57 move = rule.format(unifier, self.world.functions)
5874 self.update_state(unifier, rule)
59 return move
75 return Event(rule, unifier)
6076
6177 def get_candidate_rules(self):
6278 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))
6782
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:")
7785 for rule, unifiers in candidates:
78 print rule.nu_format()
79 print "->", unifiers
80 print
86 sys.stderr.write(rule.format())
87 sys.stderr.write("->", unifiers)
88 sys.stderr.write("")
8189
8290 return candidates
8391
8896 self.state.add(term)
8997 elif isinstance(expr, Retract):
9098 self.state.remove(term)
91 if self.debug:
99 if self.debug and False: # FIXME
92100 self.debug_state()
93101
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 print
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")
00 # encoding: UTF-8
11
2 from samovar.ast import World, Rule, Function, Situation, Cond, Assert, Retract
2 from samovar.ast import World, Scenario, Rule, Cond, Assert, Retract
33 from samovar.terms import Term, Var
44 from samovar.scanner import Scanner
55
66
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 ::= ',' | '∧'.
1923
2024
2125 class Parser(object):
2226 def __init__(self, text):
2327 self.scanner = Scanner(text)
28 self.scenario_map = {}
2429
2530 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 = []
2640 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)
3766
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()
4569
4670 def rule(self):
47 terms = []
71 words = []
4872 pre = self.cond()
4973 while not self.scanner.on('['):
50 terms.append(self.term())
74 words.append(self.word())
5175 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)
6377
6478 def cond(self):
6579 exprs = []
80 bindings = {}
6681 self.scanner.expect('[')
67 if not self.scanner.on(']'):
82 if not self.scanner.on(']') and not self.scanner.on('where'):
6883 exprs.append(self.expr())
69 while self.scanner.consume(','):
84 while self.scanner.consume(',', u'∧'):
7085 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
7192 self.scanner.expect(']')
72 return Cond(exprs=exprs)
93 return Cond(exprs=exprs, bindings=bindings)
7394
7495 def expr(self):
75 if self.scanner.consume('~'):
96 if self.scanner.consume('~', u'¬', '!'):
7697 return Retract(term=self.term())
7798 else:
7899 return Assert(term=self.term())
80101 def term(self):
81102 if self.scanner.on_type('variable'):
82103 return self.var()
83 self.scanner.check_type('word', 'punct')
104 self.scanner.check_type('word')
84105 constructor = self.scanner.token
85106 self.scanner.scan()
86107 subterms = []
89110 while self.scanner.consume(','):
90111 subterms.append(self.term())
91112 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)
93122
94123 def var(self):
95124 self.scanner.check_type('variable')
96125 name = self.scanner.token
97126 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
00 # encoding: UTF-8
11
2
3 # Python 2/3
4 try:
5 unicode = unicode
6 except NameError:
7 unicode = str
8
9
210 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 ]
338
439
540 class Scanner(object):
3368 self.token = None
3469 self.type = 'EOF'
3570 return
36 if self.scan_pattern(ur'\~|→', 'operator'):
71 if self.scan_pattern(u'\\~|→|=|¬|∧|∨', 'operator'):
3772 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'):
3977 return
4078 if self.scan_pattern(r'\(|\)|\{|\}|\[|\]', 'bracket'):
4179 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'):
4381 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)
4588 return
4689 if self.scan_pattern(r'.', 'unknown character'):
4790 return
66109 raise SyntaxError(u"Expected %s, but found %s ('%s') (near '%s')" %
67110 (types, self.type, self.token, self.near_text()))
68111
69 def consume(self, token):
70 if self.token == token:
112 def consume(self, *tokens):
113 if self.token in tokens:
71114 self.scan()
72115 return True
73116 else:
00 # encoding: UTF-8
11
22
3 # Python 2/3
4 try:
5 unicode = unicode
6 except NameError:
7 unicode = str
8
9
310 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
2112
2213
2314 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:]
2925
3026 def __str__(self):
3127 if len(self.subterms) == 0:
3430
3531 def __repr__(self):
3632 if self.subterms:
37 return "%s(%r, subterms=%r)" % (
33 return "%s(%r, *%r)" % (
3834 self.__class__.__name__, self.constructor, self.subterms
3935 )
4036 else:
4339 )
4440
4541 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)
5646
5747 def is_atom(self):
5848 return len(self.subterms) == 0
6353 return False
6454 return True
6555
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):
8157 if self.constructor != term.constructor:
8258 raise ValueError("`%s` != `%s`" % (self.constructor, term.constructor))
8359 if len(self.subterms) != len(term.subterms):
8460 raise ValueError("`%s` != `%s`" % (len(self.subterms), len(term.subterms)))
8561 for (subpat, subterm) in zip(self.subterms, term.subterms):
86 subpat.match(subterm, unifier)
62 env = subpat.match(subterm, env, **kwargs)
63 return env
8764
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])
9067
9168 def collect_atoms(self, atoms):
9269 if self.is_atom():
11188 return "%s(%r)" % (self.__class__.__name__, self.name)
11289
11390 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)
11795
11896 def is_atom(self):
11997 return False
12199 def is_ground(term):
122100 return False
123101
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)
127106 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)])
129111
130 def subst(self, unifier):
131 return unifier[self.name]
112 def subst(self, env):
113 return env[self.name]
132114
133115 def collect_atoms(self, atoms):
134116 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()
0 #!/bin/sh
1
2 if [ "x$PYTHON" = "x" ]; then
3 PYTHON="python2.7"
4 fi
5 PYTHONPATH=src $PYTHON src/samovar/tests.py || exit 1
6 falderal -b doc/Samovar.md || exit 1