├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── prolog_shell.py
├── run_tests.sh
├── samples
├── cut_test.pl
├── hanoi1.pl
├── hanoi2.pl
├── kb1.pl
├── not_test.pl
└── or_test.pl
├── setup.py
├── tests
├── test_basics.py
├── test_builtins.py
├── test_embedding.py
└── test_negation.py
└── zamiaprolog
├── __init__.py
├── builtins.py
├── errors.py
├── logic.py
├── logicdb.py
├── model.py
├── parser.py
└── runtime.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # use glob syntax.
2 | syntax: glob
3 | *.swp
4 | *.swo
5 | *.pyc
6 | tmp
7 | old
8 | data
9 | *.log
10 | TODO
11 | NOTES
12 | foo.db
13 | tests/zp_profile.py
14 | tests/zp_stats
15 | dist
16 | zamia_prolog.egg-info
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean dist upload
2 |
3 | all: dist
4 |
5 | dist:
6 | python setup.py sdist
7 |
8 | upload:
9 | twine upload dist/*
10 |
11 | clean:
12 | rm -rf build dist foo.db zamia_prolog.egg-info
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zamia Prolog
2 |
3 | Scalable and embeddable compiler/interpreter for a Zamia-Prolog (a Prolog dialect). Stores its knowledge base in a
4 | Database via SQLAlchemy - hence the scalability, i.e. the knowledge base is not limited by the amount of RAM available.
5 |
6 | Zamia-Prolog is written in pure python so it can be easily embedded into other python applications. Compiler and runtime
7 | have interfaces to register custom builtins which can either be evaluated at compile time (called directives in
8 | Zamia-Prolog) or at runtime.
9 |
10 | The Prolog core is based on http://openbookproject.net/py4fun/prolog/prolog3.html by Chris Meyers.
11 |
12 | While performance is definitely important, right now Chris' interpreted approach is more than good enough for my needs.
13 |
14 | My main focus here is embedding and language features - at the time of this writing I am experimenting with
15 | incorporating some imperative concepts into Zamia-Prolog, such as re-assignable variables and if/then/else constructs.
16 | So please note that this is a Prolog dialect that probably never will be compliant to any Prolog standards. Instead it will
17 | most likely drift further away from standard prolog and may evolve into my own logic-based language.
18 |
19 | Features
20 | ========
21 |
22 | * pure Python implementation
23 | * easy to embed in Python applications
24 | * easy to extend with custom builtins for domain specific tasks
25 | * re-assignable variables with full backtracking support
26 | * assertz/retract with full backtracking support (using database overlays)
27 | * imperative language constructs such as if/then/else
28 | * pseudo-variables/-predicates that make DB assertz/retractz easier to code
29 |
30 | Requirements
31 | ============
32 |
33 | *Note*: probably incomplete.
34 |
35 | * Python 2.7
36 | * py-nltools
37 | * SQLAlchemy
38 |
39 | Usage
40 | =====
41 |
42 | Compile `hanoi1.pl` example:
43 |
44 | ```python
45 | from zamiaprolog.logicdb import LogicDB
46 | from zamiaprolog.parser import PrologParser
47 |
48 | db_url = 'sqlite:///foo.db'
49 | db = LogicDB(db_url)
50 | parser = PrologParser()
51 |
52 | parser.compile_file('samples/hanoi1.pl', 'unittests', db)
53 | ```
54 |
55 | now run a sample goal:
56 |
57 | ```python
58 | from zamiaprolog.runtime import PrologRuntime
59 |
60 | clause = parser.parse_line_clause_body('move(3,left,right,center)')
61 | rt = PrologRuntime(db)
62 |
63 | solutions = rt.search(clause)
64 | ```
65 |
66 | output:
67 |
68 | ```
69 | Move top disk from left to right
70 | Move top disk from left to center
71 | Move top disk from right to center
72 | Move top disk from left to right
73 | Move top disk from center to left
74 | Move top disk from center to right
75 | Move top disk from left to right
76 | ```
77 |
78 | Accessing Prolog Variables from Python
79 | --------------------------------------
80 |
81 | Set var X from python:
82 | ```python
83 | clause = parser.parse_line_clause_body('Y is X*X')
84 | solutions = rt.search(clause, {'X': NumberLiteral(3)})
85 | ```
86 |
87 | check number of solutions:
88 | ```python
89 | print len(solutions)
90 | ```
91 | output:
92 | ```
93 | 1
94 | ```
95 |
96 | access prolog result Y from python:
97 | ```python
98 | print solutions[0]['Y'].f
99 | ```
100 | output:
101 | ```
102 | 9
103 | ```
104 |
105 | Custom Python Builtin Predicates
106 | --------------------------------
107 |
108 | To demonstrate how to register custom predicates with the interpreter, we will
109 | introduce a python builtin to record the moves in our Hanoi example:
110 |
111 | ```python
112 | recorded_moves = []
113 |
114 | def record_move(g, rt):
115 | global recorded_moves
116 |
117 | pred = g.terms[g.inx]
118 | args = pred.args
119 |
120 | arg_from = rt.prolog_eval(args[0], g.env)
121 | arg_to = rt.prolog_eval(args[1], g.env)
122 |
123 | recorded_moves.append((arg_from, arg_to))
124 |
125 | return True
126 |
127 | rt.register_builtin('record_move', record_move)
128 |
129 |
130 | ```
131 |
132 | now, compile and run the `hanoi2.pl` example:
133 |
134 | ```python
135 | parser.compile_file('samples/hanoi2.pl', 'unittests', db)
136 | clause = parser.parse_line_clause_body('move(3,left,right,center)')
137 | solutions = rt.search(clause)
138 | ```
139 | output:
140 | ```
141 | Move top disk from left to right
142 | Move top disk from left to center
143 | Move top disk from right to center
144 | Move top disk from left to right
145 | Move top disk from center to left
146 | Move top disk from center to right
147 | Move top disk from left to right
148 | ```
149 | now, check the recorded moves:
150 | ```python
151 | print len(recorded_moves)
152 | print repr(recorded_moves)
153 | ```
154 | output:
155 | ```
156 | 7
157 | [(Predicate(left), Predicate(right)), (Predicate(left), Predicate(center)), (Predicate(right), Predicate(center)), (Predicate(left), Predicate(right)), (Predicate(center), Predicate(left)), (Predicate(center), Predicate(right)), (Predicate(left), Predicate(right))]
158 | ```
159 |
160 | Generate Multiple Bindings from Custom Predicates
161 | -------------------------------------------------
162 |
163 | Custom predicates not only can return True/False and manipulate the environment directly to generate a single binding as
164 | in
165 |
166 | ```python
167 | def custom_pred1(g, rt):
168 |
169 | rt._trace ('CALLED BUILTIN custom_pred1', g)
170 |
171 | pred = g.terms[g.inx]
172 | args = pred.args
173 | if len(args) != 1:
174 | raise PrologRuntimeError('custom_pred1: 1 arg expected.')
175 |
176 | arg_var = rt.prolog_get_variable(args[0], g.env)
177 |
178 | g.env[arg_var] = NumberLiteral(42)
179 |
180 | return True
181 | ```
182 |
183 | they can also return a list of bindings which will then result in one prolog result each. In this example,
184 | we generate 4 bindings of two variables each:
185 |
186 | ```python
187 | def multi_binder(g, rt):
188 |
189 | global recorded_moves
190 |
191 | rt._trace ('CALLED BUILTIN multi_binder', g)
192 |
193 | pred = g.terms[g.inx]
194 | args = pred.args
195 | if len(args) != 2:
196 | raise PrologRuntimeError('multi_binder: 2 args expected.')
197 |
198 | var_x = rt.prolog_get_variable(args[0], g.env)
199 | var_y = rt.prolog_get_variable(args[1], g.env)
200 |
201 | res = []
202 | for x in range(2):
203 |
204 | lx = NumberLiteral(x)
205 |
206 | for y in range(2):
207 | ly = NumberLiteral(y)
208 |
209 | res.append({var_x: lx, var_y: ly})
210 |
211 | return res
212 | ```
213 | so running
214 | ```python
215 | clause = self.parser.parse_line_clause_body('multi_binder(X,Y)')
216 | solutions = self.rt.search(clause)
217 | ```
218 | will produce 4 solutions:
219 | ```
220 | [{u'Y': 0, u'X': 0}, {u'Y': 1, u'X': 0}, {u'Y': 0, u'X': 1}, {u'Y': 1, u'X': 1}]
221 | ```
222 |
223 | Custom Compiler Directives
224 | --------------------------
225 |
226 | Besides custom builtins we can also have custom compiler-directives in Zamia-Prolog. Directives are evalutated at compile
227 | time and will not be stored in the database.
228 |
229 | Here is an example: First, register your custom directive:
230 |
231 | ```python
232 | def _custom_directive (module_name, clause, user_data):
233 | print "_custom_directive has been called. clause: %s user_data:%s" % (clause, user_data)
234 |
235 | parser.register_directive('custom_directive', _custom_directive, None)
236 | ```
237 |
238 | now, compile a piece of prolog code that uses the directive:
239 |
240 | ```python
241 | parser.parse_line_clauses('custom_directive(abc, 42, \'foo\').')
242 |
243 | ```
244 | output:
245 | ```
246 | _custom_directive has been called. clause: custom_directive(abc, 42.0, "foo"). user_data:None
247 | []
248 | ```
249 |
250 | Re-Assignable Variables
251 | -----------------------
252 |
253 | Variables can be re-assigned using the built-in special `set` (`:=`):
254 |
255 | ```prolog
256 | Z := 23, Z := 42
257 | ```
258 | this comes with full backtracking support.
259 |
260 | Pseudo-Variables/-Predicates
261 | ----------------------------
262 |
263 | This is an extension to standard prolog syntax found in Zamia-Prolog to make "variable" setting and access
264 | easier:
265 | ```
266 | C:user -> user (C, X)
267 | C:user:name -> user (C, X), name (X, Y)
268 | self:name -> name (self, X)
269 | self:label|de -> label (self, de, X)
270 | C:mem|f1ent:rdfsLabel|en -> mem (C, f1ent, X), rdfsLabel(X, en, Y).
271 |
272 | ```
273 | this works for evaluation as well as setting/asserting (left-hand and right-hand side of expressions).
274 |
275 | Example:
276 | ```prolog
277 | assertz(foo(bar, 23)), bar:foo := 42, Z := bar:foo
278 | ```
279 | will result in `Z == 42` and `foo(bar, 42)` asserted in the database.
280 |
281 | if/then/else/endif
282 | ------------------
283 |
284 | ```prolog
285 | if foo(bar) then
286 | do1, do2
287 | else
288 | do2, do3
289 | endif
290 |
291 | ```
292 | is equivalent to
293 | ```prolog
294 | or ( and (foo(bar), do1, do2), and (not(foo(bar)), do2, do3) )
295 | ```
296 |
297 | License
298 | =======
299 |
300 | My own scripts as well as the data I create is LGPLv3 licensed unless otherwise noted in the script's copyright headers.
301 |
302 | Some scripts and files are based on works of others, in those cases it is my
303 | intention to keep the original license intact. Please make sure to check the
304 | copyright headers inside for more information.
305 |
306 | Author
307 | ======
308 |
309 | * Guenter Bartsch
310 | * Chris Meyers.
311 | * Heiko Schäfer
312 |
313 |
--------------------------------------------------------------------------------
/prolog_shell.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # interactive Zamia-Prolog shell
22 | #
23 |
24 | import os
25 | import sys
26 | import logging
27 | import readline
28 | import atexit
29 |
30 | from optparse import OptionParser
31 | from StringIO import StringIO
32 |
33 | from nltools import misc
34 | from zamiaprolog.logicdb import LogicDB
35 | from zamiaprolog.parser import PrologParser, PrologError
36 | from zamiaprolog.runtime import PrologRuntime, PrologRuntimeError
37 |
38 | # logging.basicConfig(level=logging.DEBUG)
39 | logging.basicConfig(level=logging.INFO)
40 |
41 | #
42 | # init
43 | #
44 | misc.init_app ('prolog_shell')
45 | config = misc.load_config('.nlprc')
46 |
47 | #
48 | # readline, history
49 | #
50 |
51 | histfile = os.path.join(os.path.expanduser("~"), ".hal_prolog_history")
52 | try:
53 | readline.read_history_file(histfile)
54 | # default history len is -1 (infinite), which may grow unruly
55 | readline.set_history_length(1000)
56 | except IOError:
57 | pass
58 | atexit.register(readline.write_history_file, histfile)
59 |
60 | #
61 | # main
62 | #
63 |
64 | db_url = config.get('db', 'url')
65 | # db_url = 'sqlite:///tmp/foo.db'
66 |
67 | db = LogicDB(db_url)
68 | parser = PrologParser()
69 | rt = PrologRuntime(db)
70 |
71 | while True:
72 |
73 | line = raw_input ('?- ')
74 |
75 | if line == 'quit' or line == 'exit':
76 | break
77 |
78 | try:
79 | c = parser.parse_line_clause_body(line)
80 | logging.debug( "Parse result: %s" % c)
81 |
82 | logging.debug( "Searching for c:", c )
83 |
84 | solutions = rt.search(c)
85 | if len(solutions)>0:
86 | for s in solutions:
87 | print repr(s)
88 | print 'Yes.'
89 | else:
90 | print 'No.'
91 |
92 | except PrologError as e:
93 |
94 | print "*** ERROR: %s" % e
95 |
96 | except PrologRuntimeError as e:
97 |
98 | print "*** RUNTIME ERROR: %s" % e
99 |
100 |
--------------------------------------------------------------------------------
/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | nosetests
4 |
5 |
--------------------------------------------------------------------------------
/samples/cut_test.pl:
--------------------------------------------------------------------------------
1 |
2 | foo(R, X) :- X is 1, R is 'one',!.
3 | foo(R, X) :- X is 2, R is 'two',!.
4 | foo(R, X) :- R is 'many'.
5 |
6 | numbers(1).
7 | numbers(2).
8 | numbers(3).
9 | numbers(4).
10 |
11 | bar(R,X) :- numbers(X), foo(R,X).
12 |
13 |
--------------------------------------------------------------------------------
/samples/hanoi1.pl:
--------------------------------------------------------------------------------
1 | %prolog
2 |
3 |
4 | % move(N,X,Y,Z) - move N disks from peg X to peg Y, with peg Z being the
5 | % auxilliary peg
6 | %
7 | % Strategy:
8 | % Base Case: One disc - To transfer a stack consisting of 1 disc from
9 | % peg X to peg Y, simply move that disc from X to Y
10 | % Recursive Case: To transfer n discs from X to Y, do the following:
11 | % Transfer the first n-1 discs to some other peg X
12 | % Move the last disc on X to Y
13 | % Transfer the n-1 discs from X to peg Y
14 |
15 | move(1,X,Y,_) :-
16 | write('Move top disk from '),
17 | write(X),
18 | write(' to '),
19 | write(Y),
20 | nl.
21 | move(N,X,Y,Z) :-
22 | N>1,
23 | M is N-1,
24 | move(M,X,Z,Y),
25 | move(1,X,Y,_),
26 | move(M,Z,Y,X).
27 |
28 | % - note the use of "anonymous" variables _
29 | %
30 | % Here is what happens when Prolog solves the case N=3.
31 | %
32 | % ?- move(3,left,right,center).
33 | % Move top disk from left to right
34 | % Move top disk from left to center
35 | % Move top disk from right to center
36 | % Move top disk from left to right
37 | % Move top disk from center to left
38 | % Move top disk from center to right
39 | % Move top disk from left to right
40 |
41 |
--------------------------------------------------------------------------------
/samples/hanoi2.pl:
--------------------------------------------------------------------------------
1 | %prolog
2 |
3 |
4 | % move(N,X,Y,Z) - move N disks from peg X to peg Y, with peg Z being the
5 | % auxilliary peg
6 | %
7 | % Strategy:
8 | % Base Case: One disc - To transfer a stack consisting of 1 disc from
9 | % peg X to peg Y, simply move that disc from X to Y
10 | % Recursive Case: To transfer n discs from X to Y, do the following:
11 | % Transfer the first n-1 discs to some other peg X
12 | % Move the last disc on X to Y
13 | % Transfer the n-1 discs from X to peg Y
14 |
15 | %
16 | % this version calls a custom python builtin record_move to demonstrate embedding Zamia-Prolog
17 | %
18 |
19 | move(1,X,Y,_) :-
20 | write('Move top disk from '),
21 | write(X),
22 | write(' to '),
23 | write(Y),
24 | record_move(X,Y),
25 | nl.
26 | move(N,X,Y,Z) :-
27 | N>1,
28 | M is N-1,
29 | move(M,X,Z,Y),
30 | move(1,X,Y,_),
31 | move(M,Z,Y,X).
32 |
33 |
--------------------------------------------------------------------------------
/samples/kb1.pl:
--------------------------------------------------------------------------------
1 | %prolog
2 |
3 | woman(mia).
4 | woman(jody).
5 | woman(yolanda).
6 | playsAirGuitar(jody).
7 | party.
8 |
9 |
--------------------------------------------------------------------------------
/samples/not_test.pl:
--------------------------------------------------------------------------------
1 | % prolog
2 |
3 | chancellor_endtimes (angela_merkel, [1, 2, []]).
4 | chancellor_endtimes (helmut_kohl, [1,2,3]).
5 |
6 | is_chancellor(PERSON) :-
7 | chancellor_endtimes(PERSON, ENDTIMES),
8 | list_contains(ENDTIMES, []).
9 |
10 | was_chancellor(PERSON) :-
11 | chancellor_endtimes(PERSON, ENDTIMES),
12 | not (is_chancellor(PERSON)).
13 |
14 | chancellor (angela_merkel).
15 | chancellor (helmut_kohl).
16 |
17 |
--------------------------------------------------------------------------------
/samples/or_test.pl:
--------------------------------------------------------------------------------
1 | %prolog
2 |
3 | woman(mia).
4 | woman(jody).
5 | woman(yolanda).
6 |
7 | man(joe).
8 | man(fred).
9 | man(bob).
10 |
11 | child(alice).
12 | child(jeanne).
13 | child(rascal).
14 |
15 | not_dog(alice).
16 | not_dog(jeanne).
17 |
18 | human(X) :- woman(X);man(X);child(X),not_dog(X).
19 |
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import setup, find_packages
4 |
5 | EXCLUDED = ['*.tests', '*.tests.*', 'tests.*', 'tests']
6 |
7 | setup(name ='zamia-prolog',
8 | version ='0.1.0',
9 | description ='Embeddable Prolog dialect implemented in pure Python. Stores its knowlegdebase using SQLAlchemy for scalability.',
10 | long_description = open('README.md').read(),
11 | author = 'Guenter Bartsch',
12 | author_email = 'guenter@zamia.org',
13 | maintainer = 'Guenter Bartsch',
14 | maintainer_email = 'guenter@zamia.org',
15 | url = 'https://github.com/gooofy/zamia-prolog',
16 | classifiers = [
17 | 'Topic :: Software Development :: Interpreters',
18 | 'Topic :: Software Development :: Compilers',
19 | 'Intended Audience :: Developers',
20 | 'Operating System :: POSIX :: Linux',
21 | 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
22 | 'Programming Language :: Python :: 2',
23 | 'Programming Language :: Python :: 2.7',
24 | 'Programming Language :: Python :: 3',
25 | 'Programming Language :: Python :: 3.4',
26 | 'Programming Language :: Prolog',
27 | ],
28 | platforms = 'Linux',
29 | license = 'LGPLv3',
30 | package_dir = {'zamiaprolog': 'zamiaprolog'},
31 | test_suite = 'tests',
32 | packages = find_packages('.', EXCLUDED),
33 | )
34 |
35 |
--------------------------------------------------------------------------------
/tests/test_basics.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2017 Guenter Bartsch, Heiko Schaefer
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | import unittest
22 | import logging
23 | import codecs
24 |
25 | from nltools import misc
26 | from zamiaprolog.logicdb import LogicDB
27 | from zamiaprolog.parser import PrologParser
28 | from zamiaprolog.runtime import PrologRuntime
29 | from zamiaprolog.logic import *
30 | from zamiaprolog.errors import PrologError, PrologRuntimeError
31 |
32 | UNITTEST_MODULE = 'unittests'
33 |
34 | class TestZamiaProlog (unittest.TestCase):
35 |
36 | def setUp(self):
37 |
38 | #
39 | # db, store
40 | #
41 |
42 | db_url = 'sqlite:///foo.db'
43 |
44 | # setup compiler + environment
45 |
46 | self.db = LogicDB(db_url)
47 | self.parser = PrologParser(self.db)
48 | self.rt = PrologRuntime(self.db)
49 |
50 | self.db.clear_module(UNITTEST_MODULE)
51 |
52 | def tearDown(self):
53 | self.db.close()
54 |
55 | # @unittest.skip("temporarily disabled")
56 | def test_parser(self):
57 |
58 | error_catched = False
59 |
60 | try:
61 |
62 | clause = self.parser.parse_line_clause_body('say_eoa(en, "Kids are the best')
63 | logging.debug('clause: %s' % clause)
64 | except PrologError as e:
65 | error_catched = True
66 |
67 | self.assertEqual(error_catched, True)
68 |
69 | # @unittest.skip("temporarily disabled")
70 | def test_parse_line_clauses(self):
71 |
72 | line = 'time_span(TE) :- date_time_stamp(+(D, 1.0)).'
73 |
74 | tree = self.parser.parse_line_clauses(line)
75 | logging.debug (unicode(tree[0].body))
76 | self.assertEqual (tree[0].body.name, 'date_time_stamp')
77 | self.assertEqual (tree[0].head.name, 'time_span')
78 |
79 | line = 'time_span(tomorrow, TS, TE) :- context(currentTime, T), stamp_date_time(T, date(Y, M, D, H, Mn, S, "local")), date_time_stamp(date(Y, M, +(D, 1.0), 0.0, 0.0, 0.0, "local"), TS), date_time_stamp(date(Y, M, +(D, 1.0), 23.0, 59.0, 59.0, "local"), TE).'
80 |
81 | tree = self.parser.parse_line_clauses(line)
82 | logging.debug (unicode(tree[0].body))
83 | self.assertEqual (tree[0].head.name, 'time_span')
84 | self.assertEqual (tree[0].body.name, 'and')
85 | self.assertEqual (len(tree[0].body.args), 4)
86 |
87 | # @unittest.skip("temporarily disabled")
88 | def test_kb1(self):
89 |
90 | self.assertEqual (len(self.db.lookup('party', 0)), 0)
91 |
92 | self.parser.compile_file('samples/kb1.pl', UNITTEST_MODULE)
93 |
94 | self.assertEqual (len(self.db.lookup('party', 0)), 1)
95 |
96 | clause = self.parser.parse_line_clause_body('woman(X)')
97 | logging.debug('clause: %s' % clause)
98 | solutions = self.rt.search(clause)
99 | logging.debug('solutions: %s' % repr(solutions))
100 | self.assertEqual (len(solutions), 3)
101 |
102 | clause = self.parser.parse_line_clause_body('party')
103 | logging.debug('clause: %s' % clause)
104 | solutions = self.rt.search(clause)
105 | logging.debug('solutions: %s' % repr(solutions))
106 | self.assertEqual (len(solutions), 1)
107 |
108 | clause = self.parser.parse_line_clause_body('woman(fred)')
109 | logging.debug('clause: %s' % clause)
110 | solutions = self.rt.search(clause)
111 | logging.debug('solutions: %s' % repr(solutions))
112 | self.assertEqual (len(solutions), 0)
113 |
114 | # @unittest.skip("temporarily disabled")
115 | def test_parse_to_string(self):
116 |
117 | line = u'time_span(c, X, Y) :- p1(c), p2(X, Y); p3(c); p4.'
118 |
119 | line2 = u'time_span(c, X, Y) :- or(and(p1(c), p2(X, Y)), p3(c), p4).'
120 |
121 | tree = self.parser.parse_line_clauses(line)
122 | logging.debug (unicode(tree[0].body))
123 | self.assertEqual (unicode(tree[0]), line2)
124 |
125 |
126 | # @unittest.skip("temporarily disabled")
127 | def test_or(self):
128 |
129 | self.parser.compile_file('samples/or_test.pl', UNITTEST_MODULE)
130 |
131 | # self.rt.set_trace(True)
132 |
133 | solutions = self.rt.search_predicate('woman', ['X'])
134 | logging.debug('solutions: %s' % repr(solutions))
135 | self.assertEqual (len(solutions), 3)
136 |
137 | solutions = self.rt.search_predicate('human', ['X'])
138 | logging.debug('solutions: %s' % repr(solutions))
139 | self.assertEqual (len(solutions), 8)
140 |
141 | def test_or_toplevel(self):
142 |
143 | self.parser.compile_file('samples/or_test.pl', UNITTEST_MODULE)
144 |
145 | clause = self.parser.parse_line_clause_body(u'woman(mary); woman(jody)')
146 | logging.debug(u'clause: %s' % clause)
147 | solutions = self.rt.search(clause)
148 | logging.debug('solutions: %s' % repr(solutions))
149 | self.assertEqual (len(solutions), 1)
150 |
151 | def test_or_bindings(self):
152 |
153 | clause = self.parser.parse_line_clause_body(u'S is "a", or(str_append(S, "b"), str_append(S, "c"))')
154 | logging.debug(u'clause: %s' % clause)
155 | solutions = self.rt.search(clause)
156 | logging.debug('solutions: %s' % repr(solutions))
157 | self.assertEqual (len(solutions), 2)
158 | self.assertEqual (solutions[0]['S'].s, "ab")
159 | self.assertEqual (solutions[1]['S'].s, "ac")
160 |
161 | clause = self.parser.parse_line_clause_body(u'X is 42; X is 23')
162 | logging.debug(u'clause: %s' % clause)
163 | solutions = self.rt.search(clause)
164 | logging.debug('solutions: %s' % repr(solutions))
165 | self.assertEqual (len(solutions), 2)
166 |
167 |
168 | def test_var_access(self):
169 |
170 | # set var X from python:
171 |
172 | clause = self.parser.parse_line_clause_body('Y is X*X')
173 | logging.debug('clause: %s' % clause)
174 | solutions = self.rt.search(clause, {'X': NumberLiteral(3)})
175 | logging.debug('solutions: %s' % repr(solutions))
176 | self.assertEqual (len(solutions), 1)
177 |
178 | # access prolog result Y from python:
179 |
180 | self.assertEqual (solutions[0]['Y'].f, 9)
181 |
182 | def test_list_equality(self):
183 |
184 | clause = self.parser.parse_line_clause_body('[] is []')
185 | logging.debug('clause: %s' % clause)
186 | solutions = self.rt.search(clause, {})
187 | logging.debug('solutions: %s' % repr(solutions))
188 | self.assertEqual (len(solutions), 1)
189 |
190 | clause = self.parser.parse_line_clause_body('[1] is []')
191 | logging.debug('clause: %s' % clause)
192 | solutions = self.rt.search(clause, {})
193 | logging.debug('solutions: %s' % repr(solutions))
194 | self.assertEqual (len(solutions), 0)
195 |
196 | clause = self.parser.parse_line_clause_body('909442800.0 is []')
197 | logging.debug('clause: %s' % clause)
198 | solutions = self.rt.search(clause, {})
199 | logging.debug('solutions: %s' % repr(solutions))
200 | self.assertEqual (len(solutions), 0)
201 |
202 | clause = self.parser.parse_line_clause_body('[1,2,3] = [1,2,3]')
203 | logging.debug('clause: %s' % clause)
204 | solutions = self.rt.search(clause, {})
205 | logging.debug('solutions: %s' % repr(solutions))
206 | self.assertEqual (len(solutions), 1)
207 |
208 | clause = self.parser.parse_line_clause_body('[1,2,3] \\= [1,2,3,4,5]')
209 | logging.debug('clause: %s' % clause)
210 | solutions = self.rt.search(clause, {})
211 | logging.debug('solutions: %s' % repr(solutions))
212 | self.assertEqual (len(solutions), 1)
213 |
214 | def test_is(self):
215 |
216 | clause = self.parser.parse_line_clause_body('GENDER is "blubber", GENDER is wde:Male')
217 | logging.debug('clause: %s' % clause)
218 | solutions = self.rt.search(clause, {})
219 | logging.debug('solutions: %s' % repr(solutions))
220 | self.assertEqual (len(solutions), 0)
221 |
222 | # @unittest.skip("temporarily disabled")
223 | def test_list_eval(self):
224 |
225 | clause = self.parser.parse_line_clause_body('X is 23, Z is 42, Y is [X, U, Z].')
226 | solutions = self.rt.search(clause)
227 | logging.debug('solutions: %s' % repr(solutions))
228 | self.assertEqual (len(solutions), 1)
229 | self.assertEqual (len(solutions[0]['Y'].l), 3)
230 | self.assertEqual (solutions[0]['Y'].l[0].f, 23.0)
231 | self.assertTrue (isinstance(solutions[0]['Y'].l[1], Variable))
232 | self.assertEqual (solutions[0]['Y'].l[2].f, 42.0)
233 |
234 | def test_clauses_location(self):
235 |
236 | # this will trigger a runtime error since a(Y) is a predicate,
237 | # but format_str requires a literal arg
238 | clause = self.parser.parse_line_clause_body('X is format_str("%s", a(Y))')
239 | logging.debug('clause: %s' % clause)
240 | try:
241 | solutions = self.rt.search(clause, {})
242 | self.fail("we should have seen a runtime error here")
243 | except PrologRuntimeError as e:
244 | self.assertEqual (e.location.line, 1)
245 | self.assertEqual (e.location.col, 29)
246 |
247 | def test_cut(self):
248 |
249 | self.parser.compile_file('samples/cut_test.pl', UNITTEST_MODULE)
250 |
251 | # self.rt.set_trace(True)
252 |
253 | clause = self.parser.parse_line_clause_body(u'bar(R, X)')
254 | logging.debug(u'clause: %s' % clause)
255 | solutions = self.rt.search(clause)
256 | logging.debug('solutions: %s' % repr(solutions))
257 | self.assertEqual (len(solutions), 4)
258 | self.assertEqual (solutions[0]['R'].s, "one")
259 | self.assertEqual (solutions[1]['R'].s, "two")
260 | self.assertEqual (solutions[2]['R'].s, "many")
261 | self.assertEqual (solutions[3]['R'].s, "many")
262 |
263 | # @unittest.skip("temporarily disabled")
264 | def test_anon_var(self):
265 |
266 | clause = self.parser.parse_line_clause_body('_ is 23, _ is 42.')
267 | solutions = self.rt.search(clause)
268 | logging.debug('solutions: %s' % repr(solutions))
269 | self.assertEqual (len(solutions), 1)
270 |
271 | def test_nonvar(self):
272 |
273 | clause = self.parser.parse_line_clause_body(u'S is "a", nonvar(S)')
274 | logging.debug(u'clause: %s' % clause)
275 | solutions = self.rt.search(clause)
276 | logging.debug('solutions: %s' % repr(solutions))
277 | self.assertEqual (len(solutions), 1)
278 |
279 | clause = self.parser.parse_line_clause_body(u'nonvar(S)')
280 | logging.debug(u'clause: %s' % clause)
281 | solutions = self.rt.search(clause)
282 | logging.debug('solutions: %s' % repr(solutions))
283 | self.assertEqual (len(solutions), 0)
284 |
285 | def test_unify_pseudo(self):
286 |
287 | clause = self.parser.parse_line_clause_body(u'C is foo, assertz(mem(foo, bar)), if var(C:mem|bar) then C:mem|bar := 23 endif, X := C:mem|bar')
288 | logging.debug(u'clause: %s' % clause)
289 | # self.rt.set_trace(True)
290 | solutions = self.rt.search(clause)
291 | logging.debug('solutions: %s' % repr(solutions))
292 | self.assertEqual (len(solutions), 1)
293 | self.assertEqual (solutions[0]['X'].f, 23.0)
294 |
295 | if __name__ == "__main__":
296 |
297 | logging.basicConfig(level=logging.DEBUG)
298 | logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
299 |
300 | unittest.main()
301 |
302 |
--------------------------------------------------------------------------------
/tests/test_builtins.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2017 Guenter Bartsch, Heiko Schaefer
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | import unittest
22 | import logging
23 | import codecs
24 |
25 | from nltools import misc
26 | from zamiaprolog.logicdb import LogicDB
27 | from zamiaprolog.parser import PrologParser
28 | from zamiaprolog.runtime import PrologRuntime
29 |
30 | UNITTEST_MODULE = 'unittests'
31 |
32 | class TestBuiltins (unittest.TestCase):
33 |
34 | def setUp(self):
35 | #
36 | # db, store
37 | #
38 |
39 | db_url = 'sqlite:///foo.db'
40 |
41 | # setup compiler + environment
42 |
43 | self.db = LogicDB(db_url)
44 | self.parser = PrologParser(self.db)
45 | self.rt = PrologRuntime(self.db)
46 |
47 | self.db.clear_module(UNITTEST_MODULE)
48 |
49 | def tearDown(self):
50 | self.db.close()
51 |
52 | # @unittest.skip("temporarily disabled")
53 | def test_hanoi1(self):
54 |
55 | self.parser.compile_file('samples/hanoi1.pl', UNITTEST_MODULE)
56 |
57 | clause = self.parser.parse_line_clause_body('move(3,left,right,center)')
58 | logging.debug('clause: %s' % clause)
59 | solutions = self.rt.search(clause)
60 | logging.debug('solutions: %s' % repr(solutions))
61 | self.assertEqual (len(solutions), 1)
62 |
63 | # @unittest.skip("temporarily disabled")
64 | def test_lists(self):
65 |
66 | clause = self.parser.parse_line_clause_body('X is []')
67 | solutions = self.rt.search(clause)
68 | self.assertEqual (len(solutions[0]['X'].l), 0)
69 |
70 | clause = self.parser.parse_line_clause_body('L is [1,2,3,4], X is list_sum(L), Y is list_max(L), Z is list_min(L), W is list_avg(L), V is list_len(L)')
71 | solutions = self.rt.search(clause)
72 | self.assertEqual (len(solutions[0]['L'].l), 4)
73 | self.assertEqual (solutions[0]['L'].l[3].f, 4.0)
74 |
75 | self.assertEqual (solutions[0]['X'].f, 10.0)
76 | self.assertEqual (solutions[0]['Y'].f, 4.0)
77 | self.assertEqual (solutions[0]['Z'].f, 1.0)
78 | self.assertEqual (solutions[0]['W'].f, 2.5)
79 | self.assertEqual (solutions[0]['V'].f, 4.0)
80 |
81 | clause = self.parser.parse_line_clause_body('L is [1,2,3,4], list_contains(L, 2).')
82 | solutions = self.rt.search(clause)
83 | self.assertEqual (len(solutions), 1)
84 |
85 | clause = self.parser.parse_line_clause_body('L is [1,2,3,4], list_contains(L, 23).')
86 | solutions = self.rt.search(clause)
87 | self.assertEqual (len(solutions), 0)
88 |
89 | clause = self.parser.parse_line_clause_body('X is [1,2,3,4], list_nth(1, X, E).')
90 | solutions = self.rt.search(clause)
91 | self.assertEqual (solutions[0]['E'].f, 2)
92 |
93 | clause = self.parser.parse_line_clause_body('X is [1,2,3,4], length(X, L).')
94 | solutions = self.rt.search(clause)
95 | self.assertEqual (solutions[0]['L'].f, 4)
96 |
97 | clause = self.parser.parse_line_clause_body('X is [1,2,3,4], list_slice(1, 3, X, E).')
98 | solutions = self.rt.search(clause)
99 | self.assertEqual (len(solutions[0]['E'].l), 2)
100 | self.assertEqual (solutions[0]['E'].l[0].f, 2.0)
101 | self.assertEqual (solutions[0]['E'].l[1].f, 3.0)
102 |
103 | clause = self.parser.parse_line_clause_body('X is [1,2,3,4], E is list_slice(1, 3, X).')
104 | solutions = self.rt.search(clause)
105 | self.assertEqual (len(solutions[0]['E'].l), 2)
106 | self.assertEqual (solutions[0]['E'].l[0].f, 2.0)
107 | self.assertEqual (solutions[0]['E'].l[1].f, 3.0)
108 |
109 | clause = self.parser.parse_line_clause_body('X is [1,2,3,4], list_append(X, 5).')
110 | solutions = self.rt.search(clause)
111 | self.assertEqual (len(solutions[0]['X'].l), 5)
112 | self.assertEqual (solutions[0]['X'].l[4].f, 5.0)
113 |
114 | clause = self.parser.parse_line_clause_body('X is ["1","2","3","4"], list_str_join("@", X, Y).')
115 | solutions = self.rt.search(clause)
116 | self.assertEqual (solutions[0]['Y'].s, "1@2@3@4")
117 |
118 | clause = self.parser.parse_line_clause_body('X is ["1","2","3","4"], Y is list_join("@", X).')
119 | solutions = self.rt.search(clause)
120 | self.assertEqual (solutions[0]['Y'].s, "1@2@3@4")
121 |
122 | # @unittest.skip("temporarily disabled")
123 | def test_list_findall(self):
124 |
125 | self.parser.compile_file('samples/kb1.pl', UNITTEST_MODULE)
126 |
127 | clause = self.parser.parse_line_clause_body('list_findall(X, woman(X), L)')
128 | solutions = self.rt.search(clause)
129 | self.assertEqual (len(solutions[0]), 1)
130 | self.assertEqual (len(solutions[0]['L'].l), 3)
131 |
132 | # @unittest.skip("temporarily disabled")
133 | def test_strings(self):
134 |
135 | clause = self.parser.parse_line_clause_body('X is \'bar\', S is format_str(\'test %d %s foo\', 42, X)')
136 | solutions = self.rt.search(clause)
137 | self.assertEqual (solutions[0]['S'].s, 'test 42 bar foo')
138 |
139 | clause = self.parser.parse_line_clause_body('X is \'foobar\', sub_string(X, 0, 2, _, Y)')
140 | solutions = self.rt.search(clause)
141 | self.assertEqual (solutions[0]['Y'].s, 'fo')
142 |
143 | clause = self.parser.parse_line_clause_body('atom_chars(foo, X), atom_chars(Y, "bar").')
144 | solutions = self.rt.search(clause)
145 | self.assertEqual (solutions[0]['X'].s, 'foo')
146 | self.assertEqual (solutions[0]['Y'].name, 'bar')
147 |
148 | # @unittest.skip("temporarily disabled")
149 | def test_date_time(self):
150 |
151 | clause = self.parser.parse_line_clause_body('get_time(T)')
152 | solutions = self.rt.search(clause)
153 | self.assertGreater (solutions[0]['T'].s, '2017-04-30T23:39:29.092271')
154 |
155 | clause = self.parser.parse_line_clause_body('date_time_stamp(date(2017,2,14,1,2,3,\'local\'), TS), stamp_date_time(TS, date(Y,M,D,H,Mn,S,\'local\'))')
156 | solutions = self.rt.search(clause)
157 | self.assertEqual (solutions[0]['Y'].f, 2017)
158 | self.assertEqual (solutions[0]['M'].f, 2)
159 | self.assertEqual (solutions[0]['D'].f, 14)
160 | self.assertEqual (solutions[0]['H'].f, 1)
161 | self.assertEqual (solutions[0]['Mn'].f, 2)
162 | self.assertEqual (solutions[0]['S'].f, 3)
163 |
164 | clause = self.parser.parse_line_clause_body('date_time_stamp(date(2017,2,14,1,2,3,\'Europe/Berlin\'), TS), day_of_the_week(TS, WD)')
165 | solutions = self.rt.search(clause)
166 | self.assertEqual (solutions[0]['TS'].s, '2017-02-14T01:02:03+01:00')
167 | self.assertEqual (solutions[0]['WD'].f, 2)
168 |
169 | # @unittest.skip("temporarily disabled")
170 | def test_arith(self):
171 | clause = self.parser.parse_line_clause_body('X is -23')
172 | solutions = self.rt.search(clause)
173 | self.assertEqual (solutions[0]['X'].f, -23)
174 |
175 | clause = self.parser.parse_line_clause_body('X is +42')
176 | solutions = self.rt.search(clause)
177 | self.assertEqual (solutions[0]['X'].f, 42)
178 |
179 | clause = self.parser.parse_line_clause_body('X is 19 + 23')
180 | solutions = self.rt.search(clause)
181 | self.assertEqual (solutions[0]['X'].f, 42)
182 |
183 | clause = self.parser.parse_line_clause_body('X is 61 - 19')
184 | solutions = self.rt.search(clause)
185 | self.assertEqual (solutions[0]['X'].f, 42)
186 |
187 | clause = self.parser.parse_line_clause_body('X is 6*7')
188 | solutions = self.rt.search(clause)
189 | self.assertEqual (solutions[0]['X'].f, 42)
190 |
191 | clause = self.parser.parse_line_clause_body('X is 1764 / 42')
192 | solutions = self.rt.search(clause)
193 | self.assertEqual (solutions[0]['X'].f, 42)
194 |
195 | clause = self.parser.parse_line_clause_body('X is 85 mod 43')
196 | solutions = self.rt.search(clause)
197 | self.assertEqual (solutions[0]['X'].f, 42)
198 |
199 | clause = self.parser.parse_line_clause_body('X is 23, increment(X, 19)')
200 | solutions = self.rt.search(clause)
201 | self.assertEqual (solutions[0]['X'].f, 42)
202 |
203 | clause = self.parser.parse_line_clause_body('X is 42, decrement(X, 19)')
204 | solutions = self.rt.search(clause)
205 | self.assertEqual (solutions[0]['X'].f, 23)
206 |
207 | # @unittest.skip("temporarily disabled")
208 | def test_comp(self):
209 |
210 | clause = self.parser.parse_line_clause_body('3>1')
211 | solutions = self.rt.search(clause)
212 | self.assertEqual (len(solutions), 1)
213 | clause = self.parser.parse_line_clause_body('1>1')
214 | solutions = self.rt.search(clause)
215 | self.assertEqual (len(solutions), 0)
216 |
217 | clause = self.parser.parse_line_clause_body('3<1')
218 | solutions = self.rt.search(clause)
219 | self.assertEqual (len(solutions), 0)
220 | clause = self.parser.parse_line_clause_body('1<1')
221 | solutions = self.rt.search(clause)
222 | self.assertEqual (len(solutions), 0)
223 |
224 | clause = self.parser.parse_line_clause_body('3=<1')
225 | solutions = self.rt.search(clause)
226 | self.assertEqual (len(solutions), 0)
227 | clause = self.parser.parse_line_clause_body('1=<1')
228 | solutions = self.rt.search(clause)
229 | self.assertEqual (len(solutions), 1)
230 |
231 | clause = self.parser.parse_line_clause_body('3>=1')
232 | solutions = self.rt.search(clause)
233 | self.assertEqual (len(solutions), 1)
234 | clause = self.parser.parse_line_clause_body('1>=1')
235 | solutions = self.rt.search(clause)
236 | self.assertEqual (len(solutions), 1)
237 |
238 | clause = self.parser.parse_line_clause_body('3\\=1')
239 | solutions = self.rt.search(clause)
240 | self.assertEqual (len(solutions), 1)
241 | clause = self.parser.parse_line_clause_body('1\\=1')
242 | solutions = self.rt.search(clause)
243 | self.assertEqual (len(solutions), 0)
244 |
245 | clause = self.parser.parse_line_clause_body('3=1')
246 | solutions = self.rt.search(clause)
247 | self.assertEqual (len(solutions), 0)
248 | clause = self.parser.parse_line_clause_body('1=1')
249 | solutions = self.rt.search(clause)
250 | self.assertEqual (len(solutions), 1)
251 |
252 | # @unittest.skip("temporarily disabled")
253 | def test_between(self):
254 | clause = self.parser.parse_line_clause_body('between(1,100,42)')
255 | solutions = self.rt.search(clause)
256 | self.assertEqual (len(solutions), 1)
257 |
258 | clause = self.parser.parse_line_clause_body('between(1,100,X)')
259 | solutions = self.rt.search(clause)
260 | self.assertEqual (len(solutions), 100)
261 |
262 | # @unittest.skip("temporarily disabled")
263 | def test_dicts(self):
264 |
265 | clause = self.parser.parse_line_clause_body('dict_put(U, foo, 42), X is U, dict_put(X, bar, 23), dict_get(X, Y, Z), dict_get(X, foo, V)')
266 | solutions = self.rt.search(clause)
267 | self.assertEqual (len(solutions[0]['U'].d), 1)
268 | self.assertEqual (len(solutions[0]['X'].d), 2)
269 | self.assertEqual (solutions[0]['Z'].f, 42)
270 | self.assertEqual (solutions[0]['V'].f, 42)
271 | self.assertEqual (solutions[1]['Z'].f, 23)
272 | self.assertEqual (solutions[1]['V'].f, 42)
273 |
274 | logging.debug(repr(solutions))
275 |
276 | # @unittest.skip("temporarily disabled")
277 | def test_assertz(self):
278 |
279 | clause = self.parser.parse_line_clause_body('I is ias00001, assertz(frame (I, qIsFamiliar)), frame (ias00001, X)')
280 | solutions = self.rt.search(clause)
281 | logging.debug(repr(solutions))
282 | self.assertEqual (len(solutions), 1)
283 | self.assertEqual (solutions[0]['X'].name, 'qIsFamiliar')
284 |
285 | # @unittest.skip("temporarily disabled")
286 | def test_retract(self):
287 |
288 | clause = self.parser.parse_line_clause_body('I is ias1, assertz(frame (I, a, x)), retract(frame (I, _, _)), assertz(frame (I, a, y)), frame(ias1, a, X)')
289 | solutions = self.rt.search(clause)
290 | logging.debug(repr(solutions))
291 | self.assertEqual (len(solutions), 1)
292 | self.assertEqual (solutions[0]['X'].name, 'y')
293 |
294 | # @unittest.skip("temporarily disabled")
295 | def test_retract_db(self):
296 |
297 | clause = self.parser.parse_line_clause_body('I is ias1, assertz(frame (I, a, x))')
298 | solutions = self.rt.search(clause)
299 | self.assertEqual (len(solutions), 1)
300 |
301 | clause = self.parser.parse_line_clause_body('frame(ias1, a, X)')
302 | s2s = self.rt.search(clause)
303 | self.assertEqual (len(s2s), 0)
304 |
305 | self.rt.apply_overlay(UNITTEST_MODULE, solutions[0])
306 |
307 | clause = self.parser.parse_line_clause_body('frame(ias1, a, X)')
308 | s2s = self.rt.search(clause)
309 | self.assertEqual (len(s2s), 1)
310 | self.assertEqual (s2s[0]['X'].name, 'x')
311 |
312 | clause = self.parser.parse_line_clause_body('retract(frame (ias1, _, _)), frame(ias1, a, X)')
313 | s2s = self.rt.search(clause)
314 | self.assertEqual (len(s2s), 0)
315 |
316 | clause = self.parser.parse_line_clause_body('retract(frame (ias1, _, _))')
317 | solutions = self.rt.search(clause)
318 |
319 | self.rt.apply_overlay(UNITTEST_MODULE, solutions[0])
320 |
321 | clause = self.parser.parse_line_clause_body('frame(ias1, a, X)')
322 | s2s = self.rt.search(clause)
323 | self.assertEqual (len(s2s), 0)
324 |
325 | # @unittest.skip("temporarily disabled")
326 | def test_setz(self):
327 |
328 | clause = self.parser.parse_line_clause_body('assertz(frame (ias1, a, x)), assertz(frame (ias1, a, y)), setz(frame (ias1, a, _), z), frame (ias1, a, X)')
329 | solutions = self.rt.search(clause)
330 | logging.debug(repr(solutions))
331 | self.assertEqual (len(solutions), 1)
332 | self.assertEqual (solutions[0]['X'].name, 'z')
333 |
334 | # @unittest.skip("temporarily disabled")
335 | def test_setz_multi(self):
336 |
337 | # self.rt.set_trace(True)
338 |
339 | clause = self.parser.parse_line_clause_body('I is ias2, setz(ias (I, a, _), a), setz(ias (I, b, _), b), setz(ias (I, c, _), c), ias(I, X, Y). ')
340 | solutions = self.rt.search(clause)
341 | logging.debug(repr(solutions))
342 | self.assertEqual (len(solutions), 3)
343 | self.assertEqual (solutions[0]['X'].name, 'a')
344 | self.assertEqual (solutions[0]['Y'].name, 'a')
345 | self.assertEqual (solutions[1]['X'].name, 'b')
346 | self.assertEqual (solutions[1]['Y'].name, 'b')
347 | self.assertEqual (solutions[2]['X'].name, 'c')
348 | self.assertEqual (solutions[2]['Y'].name, 'c')
349 |
350 | # @unittest.skip("temporarily disabled")
351 | def test_gensym(self):
352 |
353 | logging.debug ('test_gensym...')
354 |
355 | clause = self.parser.parse_line_clause_body('gensym(foo, I), gensym(foo, J)')
356 | solutions = self.rt.search(clause)
357 | logging.debug(repr(solutions))
358 | self.assertEqual (len(solutions), 1)
359 | self.assertNotEqual (solutions[0]['I'].name, solutions[0]['J'].name)
360 |
361 | logging.debug ('test_gensym... done.')
362 |
363 | # @unittest.skip("temporarily disabled")
364 | def test_sets(self):
365 |
366 | clause = self.parser.parse_line_clause_body('set_add(S, 42), set_add(S, 23), set_add(S, 23), set_get(S, V)')
367 | solutions = self.rt.search(clause)
368 | self.assertEqual (len(solutions[0]), 2)
369 | self.assertEqual (len(solutions[0]['S'].s), 2)
370 |
371 | logging.debug(repr(solutions))
372 |
373 | # @unittest.skip("temporarily disabled")
374 | def test_set_findall(self):
375 |
376 | self.parser.compile_file('samples/kb1.pl', UNITTEST_MODULE)
377 |
378 | clause = self.parser.parse_line_clause_body('set_findall(X, woman(X), S)')
379 | solutions = self.rt.search(clause)
380 | self.assertEqual (len(solutions[0]), 1)
381 | self.assertEqual (len(solutions[0]['S'].s), 3)
382 |
383 | # @unittest.skip("temporarily disabled")
384 | def test_eval_functions(self):
385 |
386 | clause = self.parser.parse_line_clause_body('X is [23, 42], Y is [list_avg(X), list_sum(Z)]')
387 | solutions = self.rt.search(clause)
388 | logging.debug(repr(solutions))
389 | self.assertEqual (len(solutions), 1)
390 | self.assertEqual (len(solutions[0]['Y'].l), 2)
391 |
392 | # @unittest.skip("temporarily disabled")
393 | def test_set(self):
394 |
395 | clause = self.parser.parse_line_clause_body('set(X, 23), set(X, 42), set(Y, 23), Z := Y * 2')
396 | solutions = self.rt.search(clause)
397 | logging.debug(repr(solutions))
398 | self.assertEqual (len(solutions), 1)
399 | self.assertEqual (solutions[0]['X'].f, 42)
400 | self.assertEqual (solutions[0]['Y'].f, 23)
401 | self.assertEqual (solutions[0]['Z'].f, 46)
402 |
403 | # @unittest.skip("temporarily disabled")
404 | def test_set_pseudo(self):
405 |
406 | clause = self.parser.parse_line_clause_body('assertz(foo(bar, 23)), set(bar:foo, 42), foo(bar, X), Z := bar:foo')
407 | # self.rt.set_trace(True)
408 | solutions = self.rt.search(clause)
409 | logging.debug(repr(solutions))
410 | self.assertEqual (len(solutions), 1)
411 | self.assertEqual (solutions[0]['X'].f, 42)
412 |
413 | if __name__ == "__main__":
414 |
415 | logging.basicConfig(level=logging.DEBUG)
416 | logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
417 |
418 | unittest.main()
419 |
420 |
--------------------------------------------------------------------------------
/tests/test_embedding.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2017 Guenter Bartsch, Heiko Schaefer
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | import unittest
22 | import logging
23 | import codecs
24 |
25 | from nltools import misc
26 | from zamiaprolog.logicdb import LogicDB
27 | from zamiaprolog.parser import PrologParser
28 | from zamiaprolog.runtime import PrologRuntime
29 | from zamiaprolog.logic import NumberLiteral
30 |
31 | UNITTEST_MODULE = 'unittests'
32 |
33 | recorded_moves = []
34 |
35 | def record_move(g, rt):
36 |
37 | global recorded_moves
38 |
39 | rt._trace ('CALLED BUILTIN record_move', g)
40 |
41 | pred = g.terms[g.inx]
42 | args = pred.args
43 | if len(args) != 2:
44 | raise PrologRuntimeError('record_move: 2 args expected.')
45 |
46 | arg_from = rt.prolog_eval(args[0], g.env, g.location)
47 | arg_to = rt.prolog_eval(args[1], g.env, g.location)
48 |
49 | recorded_moves.append((arg_from, arg_to))
50 |
51 | return True
52 |
53 | def multi_binder(g, rt):
54 |
55 | global recorded_moves
56 |
57 | rt._trace ('CALLED BUILTIN multi_binder', g)
58 |
59 | pred = g.terms[g.inx]
60 | args = pred.args
61 | if len(args) != 2:
62 | raise PrologRuntimeError('multi_binder: 2 args expected.')
63 |
64 | var_x = rt.prolog_get_variable(args[0], g.env, g.location)
65 | var_y = rt.prolog_get_variable(args[1], g.env, g.location)
66 |
67 | res = []
68 | for x in range(2):
69 |
70 | lx = NumberLiteral(x)
71 |
72 | for y in range(2):
73 | ly = NumberLiteral(y)
74 |
75 | res.append({var_x: lx, var_y: ly})
76 |
77 | return res
78 |
79 | class TestEmbeddings (unittest.TestCase):
80 |
81 | def setUp(self):
82 |
83 | #
84 | # db, store
85 | #
86 |
87 | db_url = 'sqlite:///foo.db'
88 |
89 | # setup compiler + environment
90 |
91 | self.db = LogicDB(db_url, echo=False)
92 | self.parser = PrologParser(self.db)
93 | self.rt = PrologRuntime(self.db)
94 |
95 | # self.rt.set_trace(True)
96 |
97 | self.db.clear_module(UNITTEST_MODULE)
98 |
99 | def tearDown(self):
100 | self.db.close()
101 |
102 | #@unittest.skip("temporarily disabled")
103 | def test_custom_builtins(self):
104 |
105 | global recorded_moves
106 |
107 | self.parser.compile_file('samples/hanoi2.pl', UNITTEST_MODULE)
108 |
109 | clause = self.parser.parse_line_clause_body('move(3,left,right,center)')
110 | logging.debug('clause: %s' % clause)
111 |
112 | # register our custom builtin
113 | recorded_moves = []
114 | self.rt.register_builtin('record_move', record_move)
115 |
116 | solutions = self.rt.search(clause)
117 | logging.debug('solutions: %s' % repr(solutions))
118 | self.assertEqual (len(solutions), 1)
119 |
120 | self.assertEqual (len(recorded_moves), 7)
121 |
122 | #@unittest.skip("temporarily disabled")
123 | def test_custom_builtin_multiple_bindings(self):
124 |
125 | self.rt.register_builtin('multi_binder', multi_binder)
126 |
127 | clause = self.parser.parse_line_clause_body('multi_binder(X,Y)')
128 | logging.debug('clause: %s' % clause)
129 | solutions = self.rt.search(clause)
130 | logging.debug('solutions: %s' % repr(solutions))
131 | self.assertEqual (len(solutions), 4)
132 |
133 | def _custom_directive(self, db, module_name, clause, user_data):
134 | # logging.debug('custom_directive has been run')
135 | self.assertEqual (len(clause.head.args), 3)
136 | self.assertEqual (unicode(clause.head.args[0]), u'abc')
137 | self.assertEqual (clause.head.args[1].f, 42)
138 | self.assertEqual (clause.head.args[2].s, u'foo')
139 |
140 | self.directive_mark = True
141 |
142 | #@unittest.skip("temporarily disabled")
143 | def test_custom_directives(self):
144 |
145 | self.parser.register_directive('custom_directive', self._custom_directive, None)
146 | self.directive_mark = False
147 |
148 | # self.parser.compile_file('samples/dir.pl', UNITTEST_MODULE)
149 | clauses = self.parser.parse_line_clauses('custom_directive(abc, 42, \'foo\').')
150 |
151 | self.assertEqual (self.directive_mark, True)
152 |
153 |
154 | if __name__ == "__main__":
155 |
156 | logging.basicConfig(level=logging.DEBUG)
157 | # logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
158 |
159 | unittest.main()
160 |
161 |
--------------------------------------------------------------------------------
/tests/test_negation.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2017 Guenter Bartsch, Heiko Schaefer
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | import unittest
22 | import logging
23 | import codecs
24 |
25 | from nltools import misc
26 | from zamiaprolog.logicdb import LogicDB
27 | from zamiaprolog.parser import PrologParser
28 | from zamiaprolog.runtime import PrologRuntime
29 | from zamiaprolog.logic import *
30 |
31 | UNITTEST_MODULE = 'unittests'
32 |
33 | class TestNegation (unittest.TestCase):
34 |
35 | def setUp(self):
36 |
37 | #
38 | # db, store
39 | #
40 |
41 | db_url = 'sqlite:///foo.db'
42 |
43 | # setup compiler + environment
44 |
45 | self.db = LogicDB(db_url)
46 | self.parser = PrologParser(self.db)
47 | self.rt = PrologRuntime(self.db)
48 |
49 | self.db.clear_module(UNITTEST_MODULE)
50 |
51 | self.rt.set_trace(True)
52 |
53 | def tearDown(self):
54 | self.db.close()
55 |
56 | # @unittest.skip("temporarily disabled")
57 | def test_not_succ(self):
58 |
59 | clause = self.parser.parse_line_clause_body('X is 1, Y is 2, not(X is Y).')
60 | logging.debug('clause: %s' % clause)
61 | solutions = self.rt.search(clause, {})
62 | logging.debug('solutions: %s' % repr(solutions))
63 | self.assertEqual (len(solutions), 1)
64 |
65 | # @unittest.skip("temporarily disabled")
66 | def test_not_fail(self):
67 | clause = self.parser.parse_line_clause_body('X is 2, Y is 2, not(X is Y).')
68 | logging.debug('clause: %s' % clause)
69 | solutions = self.rt.search(clause, {})
70 | logging.debug('solutions: %s' % repr(solutions))
71 | self.assertEqual (len(solutions), 0)
72 |
73 | # @unittest.skip("temporarily disabled")
74 | def test_chancellors(self):
75 |
76 | self.parser.compile_file('samples/not_test.pl', UNITTEST_MODULE)
77 |
78 | clause = self.parser.parse_line_clause_body('was_chancellor(helmut_kohl).')
79 | logging.debug('clause: %s' % clause)
80 | solutions = self.rt.search(clause, {})
81 | logging.debug('solutions: %s' % repr(solutions))
82 | self.assertEqual (len(solutions), 1)
83 |
84 | # @unittest.skip("temporarily disabled")
85 | def test_double_negation(self):
86 |
87 | self.parser.compile_file('samples/not_test.pl', UNITTEST_MODULE)
88 |
89 | clause = self.parser.parse_line_clause_body('not(not(chancellor(helmut_kohl))).')
90 | logging.debug('clause: %s' % clause)
91 | solutions = self.rt.search(clause, {})
92 | logging.debug('solutions: %s' % repr(solutions))
93 | self.assertEqual (len(solutions), 1)
94 |
95 | clause = self.parser.parse_line_clause_body('not(not(chancellor(angela_merkel))).')
96 | logging.debug('clause: %s' % clause)
97 | solutions = self.rt.search(clause, {})
98 | logging.debug('solutions: %s' % repr(solutions))
99 | self.assertEqual (len(solutions), 1)
100 |
101 | clause = self.parser.parse_line_clause_body('not(not(chancellor(X))).')
102 | logging.debug('clause: %s' % clause)
103 | solutions = self.rt.search(clause, {})
104 | logging.debug('solutions: %s' % repr(solutions))
105 | self.assertEqual (len(solutions), 2)
106 |
107 | # @unittest.skip("temporarily disabled")
108 | def test_assertz_negation(self):
109 |
110 | clause = self.parser.parse_line_clause_body('assertz(foobar(a)), foobar(a), (not(foobar(b))).')
111 | logging.debug('clause: %s' % clause)
112 | solutions = self.rt.search(clause, {})
113 | logging.debug('solutions: %s' % repr(solutions))
114 | self.assertEqual (len(solutions), 1)
115 |
116 | if __name__ == "__main__":
117 |
118 | logging.basicConfig(level=logging.DEBUG)
119 | logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
120 |
121 | unittest.main()
122 |
123 |
--------------------------------------------------------------------------------
/zamiaprolog/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gooofy/zamia-prolog/c9b93bc8abb13d42a6f410644056e034e5dcdb32/zamiaprolog/__init__.py
--------------------------------------------------------------------------------
/zamiaprolog/builtins.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # basic Zamia-Prolog builtins
22 | #
23 |
24 | import sys
25 | import datetime
26 | import dateutil.parser
27 | import time
28 | import logging
29 | import pytz # $ pip install pytz
30 | import copy
31 |
32 | from tzlocal import get_localzone # $ pip install tzlocal
33 |
34 | from zamiaprolog import model
35 |
36 | from zamiaprolog.logic import *
37 | from zamiaprolog.logicdb import LogicDBOverlay
38 | from zamiaprolog.errors import *
39 |
40 | PROLOG_LOGGER_NAME = 'prolog'
41 | prolog_logger = logging.getLogger(PROLOG_LOGGER_NAME)
42 |
43 | def builtin_cmp_op(g, op, rt):
44 |
45 | pred = g.terms[g.inx]
46 | args = pred.args
47 | if len(args) != 2:
48 | raise PrologRuntimeError('cmp_op: 2 args expected.', g.location)
49 |
50 | a = rt.prolog_get_literal(args[0], g.env, g.location)
51 | b = rt.prolog_get_literal(args[1], g.env, g.location)
52 |
53 | res = op(a,b)
54 |
55 | if rt.trace:
56 | logging.info("builtin_cmp_op called, a=%s, b=%s, res=%s" % (a, b, res))
57 |
58 | return res
59 |
60 | def builtin_larger(g, rt): return builtin_cmp_op(g, lambda a,b: a>b ,rt)
61 | def builtin_smaller(g, rt): return builtin_cmp_op(g, lambda a,b: a=b ,rt)
64 |
65 | def builtin_non_equal(g, rt): return builtin_cmp_op(g, lambda a,b: a!=b ,rt)
66 | def builtin_equal(g, rt): return builtin_cmp_op(g, lambda a,b: a==b ,rt)
67 |
68 | def builtin_arith_imp(g, op, rt):
69 |
70 | pred = g.terms[g.inx]
71 | args = pred.args
72 | if len(args) != 2:
73 | raise PrologRuntimeError('arith_op: 2 args expected.', g.location)
74 |
75 | a = rt.prolog_get_variable (args[0], g.env, g.location)
76 | b = rt.prolog_get_float (args[1], g.env, g.location)
77 |
78 | af = g.env[a].f if a in g.env else 0.0
79 |
80 | res = NumberLiteral(op(af,b))
81 |
82 | if rt.trace:
83 | logging.info("builtin_arith_op called, a=%s, b=%s, res=%s" % (a, b, res))
84 |
85 | g.env[a] = res
86 |
87 | return True
88 |
89 | def builtin_increment(g, rt): return builtin_arith_imp(g, lambda a,b: a+b ,rt)
90 | def builtin_decrement(g, rt): return builtin_arith_imp(g, lambda a,b: a-b ,rt)
91 |
92 | def builtin_between(g, rt):
93 |
94 | rt._trace ('CALLED BUILTIN between (+Low, +High, ?Value)', g)
95 |
96 | pred = g.terms[g.inx]
97 | args = pred.args
98 | if len(args) != 3:
99 | raise PrologRuntimeError('between: 3 args (+Low, +High, ?Value) expected.', g.location)
100 |
101 | arg_Low = rt.prolog_get_float(args[0], g.env, g.location)
102 | arg_High = rt.prolog_get_float(args[1], g.env, g.location)
103 | arg_Value = rt.prolog_eval(args[2], g.env, g.location)
104 |
105 | if isinstance(arg_Value, Variable):
106 |
107 | res = []
108 | for i in range(int(arg_Low), int(arg_High)+1):
109 | res.append({arg_Value.name: NumberLiteral(i)})
110 |
111 | return res
112 |
113 | v = arg_Value.f
114 | return ( arg_Low <= v ) and ( arg_High >= v )
115 |
116 | def builtin_date_time_stamp(g, rt):
117 |
118 | # logging.debug( "builtin_date_time_stamp called, g: %s" % g)
119 | rt._trace ('CALLED BUILTIN date_time_stamp', g)
120 |
121 | pred = g.terms[g.inx]
122 | args = pred.args
123 | if len(args) != 2:
124 | raise PrologRuntimeError('date_time_stamp: 2 args expected.', g.location)
125 |
126 | if not isinstance(args[0], Predicate) or not args[0].name == 'date' or len(args[0].args) != 7:
127 | raise PrologRuntimeError('date_time_stamp: arg0: date structure expected.', g.location)
128 |
129 | arg_Y = rt.prolog_get_int(args[0].args[0], g.env, g.location)
130 | arg_M = rt.prolog_get_int(args[0].args[1], g.env, g.location)
131 | arg_D = rt.prolog_get_int(args[0].args[2], g.env, g.location)
132 | arg_H = rt.prolog_get_int(args[0].args[3], g.env, g.location)
133 | arg_Mn = rt.prolog_get_int(args[0].args[4], g.env, g.location)
134 | arg_S = rt.prolog_get_int(args[0].args[5], g.env, g.location)
135 | arg_TZ = rt.prolog_get_string(args[0].args[6], g.env, g.location)
136 |
137 | #if pe.trace:
138 | # print "BUILTIN date_time_stamp called, Y=%s M=%s D=%s H=%s Mn=%s S=%s TZ=%s" % ( str(arg_Y), str(arg_M), str(arg_D), str(arg_H), str(arg_Mn), str(arg_S), str(arg_TZ))
139 |
140 | tz = get_localzone() if arg_TZ == 'local' else pytz.timezone(arg_TZ)
141 |
142 | if not isinstance(args[1], Variable):
143 | raise PrologRuntimeError('date_time_stamp: arg1: variable expected.', g.location)
144 |
145 | v = g.env.get(args[1].name)
146 | if v:
147 | raise PrologRuntimeError('date_time_stamp: arg1: variable already bound.', g.location)
148 |
149 | dt = datetime.datetime(arg_Y, arg_M, arg_D, arg_H, arg_Mn, arg_S, tzinfo=tz)
150 | g.env[args[1].name] = StringLiteral(dt.isoformat())
151 |
152 | return True
153 |
154 | def builtin_get_time(g, rt):
155 |
156 | rt._trace ('CALLED BUILTIN get_time', g)
157 |
158 | pred = g.terms[g.inx]
159 | args = pred.args
160 | if len(args) != 1:
161 | raise PrologRuntimeError('get_time: 1 arg expected.', g.location)
162 |
163 | arg_T = rt.prolog_get_variable(args[0], g.env, g.location)
164 |
165 | dt = datetime.datetime.now().replace(tzinfo=pytz.UTC)
166 | g.env[arg_T] = StringLiteral(dt.isoformat())
167 |
168 | return True
169 |
170 | def builtin_day_of_the_week(g, rt):
171 |
172 | """ day_of_the_week (+TS,-DayOfTheWeek) """
173 |
174 | rt._trace ('CALLED BUILTIN day_of_the_week', g)
175 |
176 | pred = g.terms[g.inx]
177 | args = pred.args
178 | if len(args) != 2:
179 | raise PrologRuntimeError('day_of_the_week: 2 args (+TS,-DayOfTheWeek) expected.', g.location)
180 |
181 | arg_TS = rt.prolog_get_string(args[0], g.env, g.location)
182 | arg_DayOfTheWeek = rt.prolog_get_variable(args[1], g.env, g.location)
183 |
184 | dt = dateutil.parser.parse(arg_TS)
185 |
186 | g.env[arg_DayOfTheWeek] = NumberLiteral(dt.weekday()+1)
187 |
188 | return True
189 |
190 | def builtin_stamp_date_time(g, rt):
191 |
192 | rt._trace ('CALLED BUILTIN stamp_date_time', g)
193 |
194 | pred = g.terms[g.inx]
195 |
196 | args = pred.args
197 | if len(args) != 2:
198 | raise PrologRuntimeError('stamp_date_time: 2 args expected.', g.location)
199 |
200 | if not isinstance(args[1], Predicate) or not args[1].name == 'date' or len(args[1].args) != 7:
201 | raise PrologRuntimeError('stamp_date_time: arg1: date structure expected.', g.location)
202 |
203 | # try:
204 | arg_Y = rt.prolog_get_variable(args[1].args[0], g.env, g.location)
205 | arg_M = rt.prolog_get_variable(args[1].args[1], g.env, g.location)
206 | arg_D = rt.prolog_get_variable(args[1].args[2], g.env, g.location)
207 | arg_H = rt.prolog_get_variable(args[1].args[3], g.env, g.location)
208 | arg_Mn = rt.prolog_get_variable(args[1].args[4], g.env, g.location)
209 | arg_S = rt.prolog_get_variable(args[1].args[5], g.env, g.location)
210 | arg_TZ = rt.prolog_get_string(args[1].args[6], g.env, g.location)
211 |
212 | tz = get_localzone() if arg_TZ == 'local' else pytz.timezone(arg_TZ)
213 |
214 | arg_TS = rt.prolog_get_string(args[0], g.env, g.location)
215 |
216 | #dt = datetime.datetime.fromtimestamp(arg_TS, tz)
217 | dt = dateutil.parser.parse(arg_TS).astimezone(tz)
218 |
219 | g.env[arg_Y] = NumberLiteral(dt.year)
220 | g.env[arg_M] = NumberLiteral(dt.month)
221 | g.env[arg_D] = NumberLiteral(dt.day)
222 | g.env[arg_H] = NumberLiteral(dt.hour)
223 | g.env[arg_Mn] = NumberLiteral(dt.minute)
224 | g.env[arg_S] = NumberLiteral(dt.second)
225 |
226 | # except PrologRuntimeError as pre:
227 | # logging.debug(pre)
228 | # import pdb; pdb.set_trace()
229 | # return False
230 |
231 | return True
232 |
233 | def builtin_sub_string(g, rt):
234 |
235 | rt._trace ('CALLED BUILTIN sub_string', g)
236 |
237 | pred = g.terms[g.inx]
238 | args = pred.args
239 | if len(args) != 5:
240 | raise PrologRuntimeError('sub_string: 5 args expected.', g.location)
241 |
242 | arg_String = rt.prolog_get_string(args[0], g.env, g.location)
243 | arg_Before = rt.prolog_eval(args[1], g.env, g.location)
244 | arg_Length = rt.prolog_eval(args[2], g.env, g.location)
245 | arg_After = rt.prolog_eval(args[3], g.env, g.location)
246 | arg_SubString = rt.prolog_eval(args[4], g.env, g.location)
247 |
248 | # FIXME: implement other variants
249 | if arg_Before:
250 | if not isinstance (arg_Before, NumberLiteral):
251 | raise PrologRuntimeError('sub_string: arg_Before: Number expected, %s found instead.' % arg_Before.__class__, g.location)
252 | before = int(arg_Before.f)
253 |
254 | if arg_Length:
255 |
256 | if not isinstance (arg_Length, NumberLiteral):
257 | raise PrologRuntimeError('sub_string: arg_Length: Number expected, %s found instead.' % arg_Length.__class__, g.location)
258 | length = int(arg_Length.f)
259 |
260 | if not isinstance(arg_After, Variable):
261 | raise PrologRuntimeError('sub_string: FIXME: arg_After required to be a variable for now.', g.location)
262 | else:
263 |
264 | var_After = arg_After.name
265 |
266 | if var_After != '_':
267 | g.env[var_After] = NumberLiteral(len(arg_String) - before - length)
268 |
269 | if not isinstance(arg_SubString, Variable):
270 | raise PrologRuntimeError('sub_string: FIXME: arg_SubString required to be a variable for now.', g.location)
271 | else:
272 | var_SubString = arg_SubString.name
273 |
274 | if var_SubString != '_':
275 | g.env[var_SubString] = StringLiteral(arg_String[before:before + length])
276 |
277 | else:
278 | raise PrologRuntimeError('sub_string: FIXME: arg_Length required to be a literal for now.', g.location)
279 | else:
280 | raise PrologRuntimeError('sub_string: FIXME: arg_Before required to be a literal for now.', g.location)
281 |
282 | return True
283 |
284 | def builtin_str_append(g, rt):
285 |
286 | """ str_append (?String, +Append) """
287 |
288 | rt._trace ('CALLED BUILTIN str_append', g)
289 |
290 | pred = g.terms[g.inx]
291 |
292 | args = pred.args
293 | if len(args) != 2:
294 | raise PrologRuntimeError('str_append: 2 args (?String, +Append) expected.', g.location)
295 |
296 | arg_str = rt.prolog_get_variable (args[0], g.env, g.location)
297 | arg_append = rt.prolog_eval (args[1], g.env, g.location)
298 |
299 | if not arg_str in g.env:
300 | g.env[arg_str] = StringLiteral(arg_append.s)
301 | else:
302 | s2 = copy.deepcopy(g.env[arg_str].s)
303 | s2 += arg_append.s
304 | g.env[arg_str] = StringLiteral(s2)
305 |
306 | return True
307 |
308 | def builtin_atom_chars(g, rt):
309 |
310 | rt._trace ('CALLED BUILTIN atom_chars', g)
311 |
312 | pred = g.terms[g.inx]
313 | args = pred.args
314 | if len(args) != 2:
315 | raise PrologRuntimeError('atom_chars: 2 args expected.', g.location)
316 |
317 | arg_atom = rt.prolog_eval(args[0], g.env, g.location)
318 | arg_str = rt.prolog_eval(args[1], g.env, g.location)
319 |
320 | if isinstance (arg_atom, Variable):
321 | if isinstance (arg_str, Variable):
322 | raise PrologRuntimeError('atom_chars: exactly one arg has to be bound.', g.location)
323 |
324 | g.env[arg_atom.name] = Predicate(arg_str.s)
325 |
326 | else:
327 | if not isinstance (arg_str, Variable):
328 | raise PrologRuntimeError('atom_chars: exactly one arg has to be bound.', g.location)
329 |
330 | g.env[arg_str.name] = StringLiteral(arg_atom.name)
331 |
332 | return True
333 |
334 | def builtin_write(g, rt):
335 |
336 | """ write (+Term) """
337 |
338 | rt._trace ('CALLED BUILTIN write', g)
339 |
340 | pred = g.terms[g.inx]
341 | args = pred.args
342 | if len(args) != 1:
343 | raise PrologRuntimeError('write: 1 arg (+Term) expected.', g.location)
344 |
345 | t = rt.prolog_eval(args[0], g.env, g.location)
346 |
347 | if isinstance (t, StringLiteral):
348 | sys.stdout.write(t.s)
349 | else:
350 | sys.stdout.write(unicode(t))
351 |
352 | return True
353 |
354 | def builtin_nl(g, rt):
355 |
356 | rt._trace ('CALLED BUILTIN nl', g)
357 |
358 | pred = g.terms[g.inx]
359 | args = pred.args
360 | if len(args) != 0:
361 | raise PrologRuntimeError('nl: no args expected.', g.location)
362 |
363 | sys.stdout.write('\n')
364 |
365 | return True
366 |
367 | def builtin_log(g, rt):
368 |
369 | """ log (+Level, +Terms...) """
370 |
371 | global prolog_logger
372 |
373 | rt._trace ('CALLED BUILTIN log', g)
374 |
375 | pred = g.terms[g.inx]
376 | args = pred.args
377 | if len(args) < 2:
378 | raise PrologRuntimeError('log: at least 2 args (+Level, +Terms...) expected.', g.location)
379 |
380 | l = rt.prolog_get_constant(args[0], g.env, g.location)
381 |
382 | s = u''
383 |
384 | for a in args[1:]:
385 | t = rt.prolog_eval (a, g.env, g.location)
386 |
387 | if len(s)>0:
388 | s += ' '
389 |
390 | if isinstance (t, StringLiteral):
391 | s += t.s
392 | else:
393 | s += unicode(t)
394 |
395 | if l == u'debug':
396 | prolog_logger.debug(s)
397 | elif l == u'info':
398 | prolog_logger.info(s)
399 | elif l == u'error':
400 | prolog_logger.error(s)
401 | else:
402 | raise PrologRuntimeError('log: unknown level %s, one of (debug, info, error) expected.' % l, g.location)
403 |
404 | return True
405 |
406 | def builtin_trace(g, rt):
407 |
408 | """ trace (+OnOff) """
409 |
410 | rt._trace ('CALLED BUILTIN trace', g)
411 |
412 | pred = g.terms[g.inx]
413 | args = pred.args
414 | if len(args) != 1:
415 | raise PrologRuntimeError('trace: 1 arg (+OnOff) expected.', g.location)
416 |
417 | onoff = rt.prolog_get_constant(args[0], g.env, g.location)
418 |
419 | if onoff == u'on':
420 | rt.set_trace(True)
421 | elif onoff == u'off':
422 | rt.set_trace(False)
423 | else:
424 | raise PrologRuntimeError('trace: unknown onoff value %s, one of (on, off) expected.' % onoff, g.location)
425 |
426 | return True
427 |
428 | def builtin_true(g, rt):
429 |
430 | """ true """
431 |
432 | rt._trace ('CALLED BUILTIN true', g)
433 |
434 | pred = g.terms[g.inx]
435 | args = pred.args
436 | if len(args) != 0:
437 | raise PrologRuntimeError('true: no args expected.', g.location)
438 |
439 | return True
440 |
441 | def builtin_ignore(g, rt):
442 |
443 | """ ignore (+P) """
444 |
445 | rt._trace ('CALLED BUILTIN ignore', g)
446 |
447 | pred = g.terms[g.inx]
448 |
449 | args = pred.args
450 | if len(args) != 1:
451 | raise PrologRuntimeError('ignore: 1 arg (+P) expected.', g.location)
452 |
453 | arg_p = args[0]
454 |
455 | if not isinstance (arg_p, Predicate):
456 | raise PrologRuntimeError('ignore: predicate expected, %s found instead.' % repr(arg_p), g.location)
457 |
458 | solutions = rt.search_predicate(arg_p.name, arg_p.args, env=g.env, location=g.location)
459 |
460 | if len(solutions)>0:
461 | return solutions
462 |
463 | return True
464 |
465 | def builtin_var(g, rt):
466 |
467 | """ var (+Term) """
468 |
469 | rt._trace ('CALLED BUILTIN var', g)
470 |
471 | pred = g.terms[g.inx]
472 |
473 | args = pred.args
474 | if len(args) != 1:
475 | raise PrologRuntimeError('var: 1 arg (+Term) expected.', g.location)
476 |
477 | arg_term = rt.prolog_eval(args[0], g.env, g.location)
478 | #import pdb; pdb.set_trace()
479 |
480 | return isinstance (arg_term, Variable)
481 |
482 | def builtin_nonvar(g, rt):
483 |
484 | """ nonvar (+Term) """
485 |
486 | rt._trace ('CALLED BUILTIN nonvar', g)
487 |
488 | pred = g.terms[g.inx]
489 |
490 | args = pred.args
491 | if len(args) != 1:
492 | raise PrologRuntimeError('nonvar: 1 arg (+Term) expected.', g.location)
493 |
494 | arg_term = rt.prolog_eval(args[0], g.env, g.location)
495 | #import pdb; pdb.set_trace()
496 |
497 | return not isinstance (arg_term, Variable)
498 |
499 | def builtin_list_contains(g, rt):
500 |
501 | rt._trace ('CALLED BUILTIN list_contains', g)
502 |
503 | pred = g.terms[g.inx]
504 |
505 | args = pred.args
506 | if len(args) != 2:
507 | raise PrologRuntimeError('list_contains: 2 args expected.', g.location)
508 |
509 | arg_list = rt.prolog_get_list (args[0], g.env, g.location)
510 | arg_needle = rt.prolog_eval(args[1], g.env, g.location)
511 |
512 | for o in arg_list.l:
513 | if o == arg_needle:
514 | return True
515 |
516 | return False
517 |
518 | def builtin_list_nth(g, rt):
519 |
520 | rt._trace ('CALLED BUILTIN list_nth', g)
521 |
522 | pred = g.terms[g.inx]
523 |
524 | args = pred.args
525 | if len(args) != 3:
526 | raise PrologRuntimeError('list_nth: 3 args (index, list, elem) expected.', g.location)
527 |
528 | arg_idx = rt.prolog_get_int (args[0], g.env, g.location)
529 | arg_list = rt.prolog_get_list (args[1], g.env, g.location)
530 | arg_elem = rt.prolog_eval (args[2], g.env, g.location)
531 | if not arg_elem:
532 | arg_elem = args[2]
533 |
534 | if not isinstance(arg_elem, Variable):
535 | raise PrologRuntimeError('list_nth: 3rd arg has to be an unbound variable for now, %s found instead.' % repr(arg_elem), g.location)
536 |
537 | g.env[arg_elem.name] = arg_list.l[arg_idx]
538 |
539 | return True
540 |
541 | def builtin_length(g, rt):
542 |
543 | """ length (+List, -Len) """
544 |
545 | rt._trace ('CALLED BUILTIN length', g)
546 |
547 | pred = g.terms[g.inx]
548 |
549 | args = pred.args
550 | if len(args) != 2:
551 | raise PrologRuntimeError('length: 2 args (+List, -Len) expected.', g.location)
552 |
553 | arg_list = rt.prolog_get_list (args[0], g.env, g.location)
554 | arg_len = rt.prolog_get_variable (args[1], g.env, g.location)
555 |
556 | g.env[arg_len] = NumberLiteral(len(arg_list.l))
557 |
558 | return True
559 |
560 | def builtin_list_slice(g, rt):
561 |
562 | """ list_slice (+Idx1, +Idx2, +List, -Slice) """
563 |
564 | rt._trace ('CALLED BUILTIN list_slice', g)
565 |
566 | pred = g.terms[g.inx]
567 |
568 | args = pred.args
569 | if len(args) != 4:
570 | raise PrologRuntimeError('list_slice: 4 args (+Idx1, +Idx2, +List, -Slice) expected.', g.location)
571 |
572 | arg_idx1 = rt.prolog_get_int (args[0], g.env, g.location)
573 | arg_idx2 = rt.prolog_get_int (args[1], g.env, g.location)
574 | arg_list = rt.prolog_get_list (args[2], g.env, g.location)
575 | arg_slice = rt.prolog_eval (args[3], g.env, g.location)
576 | if not arg_slice:
577 | arg_slice = args[3]
578 |
579 | if not isinstance(arg_slice, Variable):
580 | raise PrologRuntimeError('list_slice: 4th arg has to be an unbound variable for now, %s found instead.' % repr(arg_slice), g.location)
581 |
582 | g.env[arg_slice.name] = ListLiteral(arg_list.l[arg_idx1:arg_idx2])
583 |
584 | return True
585 |
586 | def builtin_list_append(g, rt):
587 |
588 | """ list_append (?List, +Element) """
589 |
590 | rt._trace ('CALLED BUILTIN list_append', g)
591 |
592 | pred = g.terms[g.inx]
593 |
594 | args = pred.args
595 | if len(args) != 2:
596 | raise PrologRuntimeError('list_append: 2 args (?List, +Element) expected.', g.location)
597 |
598 | arg_list = rt.prolog_get_variable (args[0], g.env, g.location)
599 | arg_element = rt.prolog_eval (args[1], g.env, g.location)
600 |
601 | if not arg_list in g.env:
602 | g.env[arg_list] = ListLiteral([arg_element])
603 | else:
604 | l2 = copy.deepcopy(g.env[arg_list].l)
605 | l2.append(arg_element)
606 | g.env[arg_list] = ListLiteral(l2)
607 |
608 | return True
609 |
610 | def do_list_extend(env, arg_list, arg_elements):
611 |
612 | if not arg_list in env:
613 | env[arg_list] = arg_elements
614 | else:
615 | l2 = copy.deepcopy(env[arg_list].l)
616 | l2.extend(arg_elements.l)
617 | env[arg_list] = ListLiteral(l2)
618 |
619 | return True
620 |
621 | def builtin_list_extend(g, rt):
622 |
623 | """ list_extend (?List, +Elements) """
624 |
625 | rt._trace ('CALLED BUILTIN list_extend', g)
626 |
627 | pred = g.terms[g.inx]
628 |
629 | args = pred.args
630 | if len(args) != 2:
631 | raise PrologRuntimeError('list_extend: 2 args (?List, +Elements) expected.', g.location)
632 |
633 | arg_list = rt.prolog_get_variable (args[0], g.env, g.location)
634 | arg_elements = rt.prolog_eval (args[1], g.env, g.location)
635 |
636 | if not isinstance(arg_elements, ListLiteral):
637 | raise PrologRuntimeError('list_extend: list type elements expected', g.location)
638 |
639 | return do_list_extend(g.env, arg_list, arg_elements)
640 |
641 | def builtin_list_str_join(g, rt):
642 |
643 | """ list_str_join (+Glue, +List, -Str) """
644 |
645 | rt._trace ('CALLED BUILTIN list_str_join', g)
646 |
647 | pred = g.terms[g.inx]
648 |
649 | args = pred.args
650 | if len(args) != 3:
651 | raise PrologRuntimeError('list_str_join: 3 args (+Glue, +List, -Str) expected.', g.location)
652 |
653 | arg_glue = rt.prolog_get_string (args[0], g.env, g.location)
654 | arg_list = rt.prolog_get_list (args[1], g.env, g.location)
655 | arg_str = rt.prolog_eval (args[2], g.env, g.location)
656 | if not arg_str:
657 | arg_str = args[2]
658 |
659 | if not isinstance(arg_str, Variable):
660 | raise PrologRuntimeError('list_str_join: 3rd arg has to be an unbound variable for now, %s found instead.' % repr(arg_slice), g.location)
661 |
662 | g.env[arg_str.name] = StringLiteral(arg_glue.join(map(lambda a: a.s if isinstance(a, StringLiteral) else unicode(a), arg_list.l)))
663 |
664 | return True
665 |
666 | def builtin_list_findall(g, rt):
667 |
668 | """ list_findall (+Template, +Goal, -List) """
669 |
670 | rt._trace ('CALLED BUILTIN list_findall', g)
671 |
672 | pred = g.terms[g.inx]
673 |
674 | args = pred.args
675 | if len(args) != 3:
676 | raise PrologRuntimeError('list_findall: 3 args (+Template, +Goal, -List) expected.', g.location)
677 |
678 | arg_tmpl = rt.prolog_get_variable (args[0], g.env, g.location)
679 | arg_goal = args[1]
680 | arg_list = rt.prolog_get_variable (args[2], g.env, g.location)
681 |
682 | if not isinstance (arg_goal, Predicate):
683 | raise PrologRuntimeError('list_findall: predicate goal expected, %s found instead.' % repr(arg_goal), g.location)
684 |
685 | solutions = rt.search_predicate(arg_goal.name, arg_goal.args, env=g.env, location=g.location)
686 |
687 | rs = []
688 | for s in solutions:
689 | rs.append(s[arg_tmpl])
690 |
691 | g.env[arg_list] = ListLiteral(rs)
692 |
693 | return True
694 |
695 | def builtin_dict_put(g, rt):
696 |
697 | """ dict_put (?Dict, +Key, +Value) """
698 |
699 | rt._trace ('CALLED BUILTIN dict_put', g)
700 |
701 | pred = g.terms[g.inx]
702 |
703 | args = pred.args
704 | if len(args) != 3:
705 | raise PrologRuntimeError('dict_put: 3 args (?Dict, +Key, +Value) expected.', g.location)
706 |
707 | arg_dict = rt.prolog_get_variable (args[0], g.env, g.location)
708 | arg_key = rt.prolog_get_constant (args[1], g.env, g.location)
709 | arg_val = rt.prolog_eval (args[2], g.env, g.location)
710 |
711 | if not arg_dict in g.env:
712 | g.env[arg_dict] = DictLiteral({arg_key: arg_val})
713 | else:
714 | d2 = copy.deepcopy(g.env[arg_dict].d)
715 | d2[arg_key] = arg_val
716 | g.env[arg_dict] = DictLiteral(d2)
717 |
718 | return True
719 |
720 | def builtin_dict_get(g, rt):
721 |
722 | """ dict_get (+Dict, ?Key, -Value) """
723 |
724 | rt._trace ('CALLED BUILTIN dict_get', g)
725 |
726 | pred = g.terms[g.inx]
727 |
728 | args = pred.args
729 | if len(args) != 3:
730 | raise PrologRuntimeError('dict_get: 3 args (+Dict, ?Key, -Value) expected.', g.location)
731 |
732 | arg_dict = rt.prolog_get_dict (args[0], g.env, g.location)
733 | arg_key = rt.prolog_eval (args[1], g.env, g.location)
734 | arg_val = rt.prolog_get_variable (args[2], g.env, g.location)
735 |
736 | res = []
737 |
738 | if isinstance(arg_key, Variable):
739 |
740 | arg_key = arg_key.name
741 |
742 | for key in arg_dict.d:
743 | res.append({arg_key: StringLiteral(key), arg_val: arg_dict.d[key]})
744 |
745 | else:
746 |
747 | arg_key = rt.prolog_get_constant (args[1], g.env, g.location)
748 | res.append({arg_val: arg_dict.d[arg_key]})
749 |
750 | return res
751 |
752 | def builtin_set_add(g, rt):
753 |
754 | """ set_add (?Set, +Value) """
755 |
756 | rt._trace ('CALLED BUILTIN set_add', g)
757 |
758 | pred = g.terms[g.inx]
759 |
760 | args = pred.args
761 | if len(args) != 2:
762 | raise PrologRuntimeError('set_add: 2 args (?Set, +Value) expected.', g.location)
763 |
764 | arg_set = rt.prolog_get_variable (args[0], g.env, g.location)
765 | arg_val = rt.prolog_eval (args[1], g.env, g.location)
766 |
767 | if not arg_set in g.env:
768 | g.env[arg_set] = SetLiteral(set([arg_val]))
769 | else:
770 | s2 = copy.deepcopy(g.env[arg_set].s)
771 | s2.add(arg_val)
772 | g.env[arg_set] = SetLiteral(s2)
773 |
774 | return True
775 |
776 | def builtin_set_get(g, rt):
777 |
778 | """ set_get (+Set, -Value) """
779 |
780 | rt._trace ('CALLED BUILTIN set_get', g)
781 |
782 | pred = g.terms[g.inx]
783 |
784 | args = pred.args
785 | if len(args) != 2:
786 | raise PrologRuntimeError('set_get: 2 args (+Set, -Value) expected.', g.location)
787 |
788 | arg_set = rt.prolog_get_set (args[0], g.env, g.location)
789 | arg_val = rt.prolog_get_variable (args[1], g.env, g.location)
790 |
791 | res = []
792 |
793 | for v in arg_set.s:
794 | res.append({arg_val: v})
795 |
796 | return res
797 |
798 | def builtin_set_findall(g, rt):
799 |
800 | """ set_findall (+Template, +Goal, -Set) """
801 |
802 | rt._trace ('CALLED BUILTIN set_findall', g)
803 |
804 | pred = g.terms[g.inx]
805 |
806 | args = pred.args
807 | if len(args) != 3:
808 | raise PrologRuntimeError('set_findall: 3 args (+Template, +Goal, -Set) expected.', g.location)
809 |
810 | arg_tmpl = rt.prolog_get_variable (args[0], g.env, g.location)
811 | arg_goal = args[1]
812 | arg_set = rt.prolog_get_variable (args[2], g.env, g.location)
813 |
814 | if not isinstance (arg_goal, Predicate):
815 | raise PrologRuntimeError('set_findall: predicate goal expected, %s found instead.' % repr(arg_goal), g.location)
816 |
817 | solutions = rt.search_predicate(arg_goal.name, arg_goal.args, env=g.env, location=g.location)
818 |
819 | rs = set()
820 | for s in solutions:
821 | rs.add(s[arg_tmpl])
822 |
823 | g.env[arg_set] = SetLiteral(rs)
824 |
825 | return True
826 |
827 | ASSERT_OVERLAY_VAR_NAME = '__OVERLAYZ__'
828 |
829 | def do_assertz(env, clause, res={}):
830 |
831 | ovl = res.get(ASSERT_OVERLAY_VAR_NAME)
832 | if ovl is None:
833 | ovl = env.get(ASSERT_OVERLAY_VAR_NAME)
834 |
835 | if ovl is None:
836 | ovl = LogicDBOverlay()
837 | else:
838 | ovl = ovl.clone()
839 |
840 | ovl.assertz(clause)
841 |
842 | # important: do not modify our (default!) argument
843 | res2 = copy.copy(res)
844 | res2[ASSERT_OVERLAY_VAR_NAME] = ovl
845 |
846 | return res2
847 |
848 | def builtin_assertz(g, rt):
849 |
850 | """ assertz (+P) """
851 |
852 | rt._trace ('CALLED BUILTIN assertz', g)
853 |
854 | pred = g.terms[g.inx]
855 |
856 | args = pred.args
857 | if len(args) != 1:
858 | raise PrologRuntimeError('assertz: 1 arg (+P) expected.', g.location)
859 |
860 | arg_p = rt.prolog_get_predicate (args[0], g.env, g.location)
861 |
862 | # if not arg_p:
863 | # import pdb; pdb.set_trace()
864 |
865 | clause = Clause (head=arg_p, location=g.location)
866 |
867 | return [do_assertz(g.env, clause, res={})]
868 |
869 | def do_assertz_predicate(env, name, args, res={}, location=None):
870 |
871 | """ convenience function: build Clause/Predicate structure, translate python string into Predicates/Variables by
872 | Prolog conventions (lowercase: predicate, uppercase: variable) """
873 |
874 | if not location:
875 | location = SourceLocation('', 0, 0)
876 |
877 | mapped_args = []
878 | for arg in args:
879 | if not isinstance(arg, basestring):
880 | mapped_args.append(arg)
881 | continue
882 | if arg[0].isupper():
883 | mapped_args.append(Variable(arg))
884 | else:
885 | mapped_args.append(Predicate(arg))
886 |
887 | return do_assertz (env, Clause(head=Predicate(name, mapped_args), location=location), res=res)
888 |
889 | def do_retract(env, p, res={}):
890 |
891 | ovl = res.get(ASSERT_OVERLAY_VAR_NAME)
892 | if ovl is None:
893 | ovl = env.get(ASSERT_OVERLAY_VAR_NAME)
894 |
895 | if ovl is None:
896 | ovl = LogicDBOverlay()
897 | else:
898 | ovl = ovl.clone()
899 |
900 | ovl.retract(p)
901 | # important: do not modify our (default!) argument
902 | res2 = copy.copy(res)
903 | res2[ASSERT_OVERLAY_VAR_NAME] = ovl
904 |
905 | return res2
906 |
907 | def builtin_retract(g, rt):
908 |
909 | """ retract (+P) """
910 |
911 | rt._trace ('CALLED BUILTIN retract', g)
912 |
913 | pred = g.terms[g.inx]
914 |
915 | args = pred.args
916 | if len(args) != 1:
917 | raise PrologRuntimeError('retract: 1 arg (+P) expected.', g.location)
918 |
919 | arg_p = rt.prolog_get_predicate (args[0], g.env, g.location)
920 |
921 | return [do_retract(g.env, arg_p, res={})]
922 |
923 |
924 | def builtin_setz(g, rt):
925 |
926 | """ setz (+P, +V) """
927 |
928 | rt._trace ('CALLED BUILTIN setz', g)
929 |
930 | # import pdb; pdb.set_trace()
931 |
932 | pred = g.terms[g.inx]
933 |
934 | args = pred.args
935 | if len(args) != 2:
936 | raise PrologRuntimeError('setz: 2 args (+P, +V) expected.', g.location)
937 |
938 | arg_p = rt.prolog_get_predicate (args[0], g.env, g.location)
939 | arg_v = rt.prolog_eval (args[1], g.env, g.location)
940 |
941 | env = do_retract(g.env, arg_p, res={})
942 |
943 | pa = []
944 | for arg in arg_p.args:
945 | if isinstance(arg, Variable):
946 | pa.append(arg_v)
947 | else:
948 | pa.append(arg)
949 |
950 | env = do_assertz (env, Clause(head=Predicate(arg_p.name, pa), location=g.location), res={})
951 |
952 | return [env]
953 |
954 | def do_gensym(rt, root):
955 |
956 | orm_gn = rt.db.session.query(model.ORMGensymNum).filter(model.ORMGensymNum.root==root).first()
957 |
958 | if not orm_gn:
959 | current_num = 1
960 | orm_gn = model.ORMGensymNum(root=root, current_num=1)
961 | rt.db.session.add(orm_gn)
962 | else:
963 | current_num = orm_gn.current_num + 1
964 | orm_gn.current_num = current_num
965 |
966 | return root + str(current_num)
967 |
968 | def builtin_gensym(g, rt):
969 |
970 | """ gensym (+Root, -Unique) """
971 |
972 | rt._trace ('CALLED BUILTIN gensym', g)
973 |
974 | pred = g.terms[g.inx]
975 |
976 | args = pred.args
977 | if len(args) != 2:
978 | raise PrologRuntimeError('gensym: 2 args (+Root, -Unique) expected.', g.location)
979 |
980 | arg_root = rt.prolog_eval (args[0], g.env, g.location)
981 | arg_unique = rt.prolog_get_variable (args[1], g.env, g.location)
982 |
983 | unique = do_gensym(rt, arg_root.name)
984 |
985 | g.env[arg_unique] = Predicate(unique)
986 |
987 | return True
988 |
989 |
990 | #
991 | # functions
992 | #
993 |
994 | def builtin_format_str(args, env, rt, location):
995 |
996 | rt._trace_fn ('CALLED FUNCTION format_str', env)
997 |
998 | arg_F = args[0].s
999 |
1000 | if len(args)>1:
1001 |
1002 | a = []
1003 | for arg in args[1:]:
1004 | if not isinstance(arg, Literal):
1005 | raise PrologRuntimeError ('format_str: literal expected, %s found instead' % arg, location)
1006 |
1007 | a = map(lambda x: x.get_literal(), args[1:])
1008 |
1009 | f_str = arg_F % tuple(a)
1010 |
1011 | else:
1012 |
1013 | f_str = arg_F
1014 |
1015 | return StringLiteral(f_str)
1016 |
1017 | def _builtin_list_lambda (args, env, rt, l, location):
1018 |
1019 | if len(args) != 1:
1020 | raise PrologRuntimeError('list builtin fn: 1 arg expected.', location)
1021 |
1022 | arg_list = args[0]
1023 | if not isinstance(arg_list, ListLiteral):
1024 | raise PrologRuntimeError('list builtin fn: list expected, %s found instead.' % arg_list, location)
1025 |
1026 | res = reduce(l, arg_list.l)
1027 | return res, arg_list.l
1028 | # if isinstance(res, (int, float)):
1029 | # return NumberLiteral(res)
1030 | # else:
1031 | # return StringLiteral(unicode(res))
1032 |
1033 | def builtin_list_max(args, env, rt, location):
1034 |
1035 | rt._trace_fn ('CALLED FUNCTION list_max', env)
1036 |
1037 | return _builtin_list_lambda (args, env, rt, lambda x, y: x if x > y else y, location)[0]
1038 |
1039 | def builtin_list_min(args, env, rt, location):
1040 |
1041 | rt._trace_fn ('CALLED FUNCTION list_min', env)
1042 |
1043 | return _builtin_list_lambda (args, env, rt, lambda x, y: x if x < y else y, location)[0]
1044 |
1045 | def builtin_list_sum(args, env, rt, location):
1046 |
1047 | rt._trace_fn ('CALLED FUNCTION list_sum', env)
1048 |
1049 | return _builtin_list_lambda (args, env, rt, lambda x, y: x + y, location)[0]
1050 |
1051 | def builtin_list_avg(args, env, rt, location):
1052 |
1053 | rt._trace_fn ('CALLED FUNCTION list_avg', env)
1054 |
1055 | l_sum, l = _builtin_list_lambda (args, env, rt, lambda x, y: x + y, location)
1056 |
1057 | assert len(l)>0
1058 | return l_sum / NumberLiteral(float(len(l)))
1059 |
1060 | def builtin_list_len(args, env, rt, location):
1061 |
1062 | rt._trace_fn ('CALLED FUNCTION list_len', env)
1063 |
1064 | if len(args) != 1:
1065 | raise PrologRuntimeError('list builtin fn: 1 arg expected.', location)
1066 |
1067 | arg_list = rt.prolog_get_list (args[0], env, location)
1068 | return NumberLiteral(len(arg_list.l))
1069 |
1070 | def builtin_list_slice_fn(args, env, rt, location):
1071 |
1072 | rt._trace_fn ('CALLED FUNCTION list_slice', env)
1073 |
1074 | if len(args) != 3:
1075 | raise PrologRuntimeError('list_slice: 3 args (+Idx1, +Idx2, +List) expected.', location)
1076 |
1077 | arg_idx1 = rt.prolog_get_int (args[0], env, location)
1078 | arg_idx2 = rt.prolog_get_int (args[1], env, location)
1079 | arg_list = rt.prolog_get_list (args[2], env, location)
1080 |
1081 | return ListLiteral(arg_list.l[arg_idx1:arg_idx2])
1082 |
1083 | def builtin_list_join_fn(args, env, rt, location):
1084 |
1085 | rt._trace_fn ('CALLED FUNCTION list_join', env)
1086 |
1087 | if len(args) != 2:
1088 | raise PrologRuntimeError('list_join: 2 args (+Glue, +List) expected.', location)
1089 |
1090 | arg_glue = rt.prolog_get_string (args[0], env, location)
1091 | arg_list = rt.prolog_get_list (args[1], env, location)
1092 |
1093 | return StringLiteral(arg_glue.join(map(lambda a: a.s if isinstance(a, StringLiteral) else unicode(a), arg_list.l)))
1094 |
1095 |
--------------------------------------------------------------------------------
/zamiaprolog/errors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # Zamia-Prolog error classes
22 | #
23 |
24 | class PrologRuntimeError(Exception):
25 | def __init__(self, value, location=None):
26 | self.value = value
27 | self.location = location
28 | def __str__(self):
29 | if self.location:
30 | return str(self.location) + ':' + repr(self.value)
31 | return repr(self.value)
32 | def __unicode__(self):
33 | if self.location:
34 | return unicode(self.location) + u':' + self.value
35 | return self.value
36 |
37 | # parser throws this at compile-time:
38 | class PrologError(Exception):
39 | def __init__(self, value, location=None):
40 | self.value = value
41 | self.location = location
42 | def __str__(self):
43 | if self.location:
44 | return str(self.location) + ':' + repr(self.value)
45 | return repr(self.value)
46 | def __unicode__(self):
47 | if self.location:
48 | return unicode(self.location) + u':' + self.value
49 | return self.value
50 |
51 |
--------------------------------------------------------------------------------
/zamiaprolog/logic.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # classes that represent prolog clauses
22 | #
23 |
24 | import logging
25 | import json
26 |
27 | from six import python_2_unicode_compatible, text_type, string_types
28 |
29 | from zamiaprolog.errors import PrologError
30 |
31 | class JSONLogic:
32 |
33 | """ just a base class that indicates to_dict() and __init__(json_dict) are supported
34 | for JSON (de)-serialization """
35 |
36 | def to_dict(self):
37 | raise PrologError ("to_dict is not implemented, but should be!")
38 |
39 | @python_2_unicode_compatible
40 | class SourceLocation(JSONLogic):
41 |
42 | def __init__ (self, fn=None, line=None, col=None, json_dict=None):
43 | if json_dict:
44 | self.fn = json_dict['fn']
45 | self.line = json_dict['line']
46 | self.col = json_dict['col']
47 | else:
48 | self.fn = fn
49 | self.line = line
50 | self.col = col
51 |
52 | def __str__(self):
53 | return u'%s: line=%s, col=%s' % (self.fn, text_type(self.line), text_type(self.col))
54 |
55 | def __repr__(self):
56 | return 'SourceLocation(fn=%s, line=%d, col=%d)' % (self.fn, self.line, self.col)
57 |
58 | def to_dict(self):
59 | return {'pt': 'SourceLocation', 'fn': self.fn, 'line': self.line, 'col': self.col}
60 |
61 | @python_2_unicode_compatible
62 | class Literal(JSONLogic):
63 |
64 | def __str__(self):
65 | return u""
66 |
67 | @python_2_unicode_compatible
68 | class StringLiteral(Literal):
69 |
70 | def __init__(self, s=None, json_dict=None):
71 | if json_dict:
72 | self.s = json_dict['s']
73 | else:
74 | self.s = s
75 |
76 | def __eq__(self, b):
77 | return isinstance(b, StringLiteral) and self.s == b.s
78 |
79 | def __lt__(self, b):
80 | assert isinstance(b, StringLiteral)
81 | return self.s < b.s
82 |
83 | def __le__(self, b):
84 | assert isinstance(b, StringLiteral)
85 | return self.s <= b.s
86 |
87 | def __ne__(self, b):
88 | return not isinstance(b, StringLiteral) or self.s != b.s
89 |
90 | def __ge__(self, b):
91 | assert isinstance(b, StringLiteral)
92 | return self.s >= b.s
93 |
94 | def __gt__(self, b):
95 | assert isinstance(b, StringLiteral)
96 | return self.s > b.s
97 |
98 | def get_literal(self):
99 | return self.s
100 |
101 | def __str__(self):
102 | return u'"' + text_type(self.s.replace('"', '\\"')) + u'"'
103 |
104 | def __repr__(self):
105 | return u'StringLiteral(' + repr(self.s) + ')'
106 |
107 | def to_dict(self):
108 | return {'pt': 'StringLiteral', 's': self.s}
109 |
110 | def __hash__(self):
111 | return hash(self.s)
112 |
113 | @python_2_unicode_compatible
114 | class NumberLiteral(Literal):
115 |
116 | def __init__(self, f=None, json_dict=None):
117 | if json_dict:
118 | self.f = json_dict['f']
119 | else:
120 | self.f = f
121 |
122 | def __str__(self):
123 | return text_type(self.f)
124 |
125 | def __repr__(self):
126 | return repr(self.f)
127 |
128 | def __eq__(self, b):
129 | return isinstance(b, NumberLiteral) and self.f == b.f
130 |
131 | def __lt__(self, b):
132 | assert isinstance(b, NumberLiteral)
133 | return self.f < b.f
134 |
135 | def __le__(self, b):
136 | assert isinstance(b, NumberLiteral)
137 | return self.f <= b.f
138 |
139 | def __ne__(self, b):
140 | return not isinstance(b, NumberLiteral) or self.f != b.f
141 |
142 | def __ge__(self, b):
143 | assert isinstance(b, NumberLiteral)
144 | return self.f >= b.f
145 |
146 | def __gt__(self, b):
147 | assert isinstance(b, NumberLiteral)
148 | return self.f > b.f
149 |
150 | def __add__(self, b):
151 | assert isinstance(b, NumberLiteral)
152 | return NumberLiteral(b.f + self.f)
153 |
154 | def __div__(self, b):
155 | assert isinstance(b, NumberLiteral)
156 | return NumberLiteral(self.f / b.f)
157 |
158 | def get_literal(self):
159 | return self.f
160 |
161 | def to_dict(self):
162 | return {'pt': 'NumberLiteral', 'f': self.f}
163 |
164 | def __hash__(self):
165 | return hash(self.f)
166 |
167 | @python_2_unicode_compatible
168 | class ListLiteral(Literal):
169 |
170 | def __init__(self, l=None, json_dict=None):
171 | if json_dict:
172 | self.l = json_dict['l']
173 | else:
174 | self.l = l
175 |
176 | def __eq__(self, other):
177 |
178 | if not isinstance(other, ListLiteral):
179 | return False
180 |
181 | return other.l == self.l
182 |
183 | def __ne__(self, other):
184 |
185 | if not isinstance(other, ListLiteral):
186 | return True
187 |
188 | return other.l != self.l
189 |
190 | def get_literal(self):
191 | return self.l
192 |
193 | def __str__(self):
194 | return u'[' + u','.join(map(lambda e: text_type(e), self.l)) + u']'
195 |
196 | def __repr__(self):
197 | return repr(self.l)
198 |
199 | def to_dict(self):
200 | return {'pt': 'ListLiteral', 'l': self.l}
201 |
202 | @python_2_unicode_compatible
203 | class DictLiteral(Literal):
204 |
205 | def __init__(self, d=None, json_dict=None):
206 | if json_dict:
207 | self.d = json_dict['d']
208 | else:
209 | self.d = d
210 |
211 | def __eq__(self, other):
212 |
213 | if not isinstance(other, DictLiteral):
214 | return False
215 |
216 | return other.d == self.d
217 |
218 | def __ne__(self, other):
219 |
220 | if not isinstance(other, DictLiteral):
221 | return True
222 |
223 | return other.d != self.d
224 |
225 | def get_literal(self):
226 | return self.d
227 |
228 | def __str__(self):
229 | return text_type(self.d)
230 |
231 | def __repr__(self):
232 | return repr(self.d)
233 |
234 | def to_dict(self):
235 | return {'pt': 'DictLiteral', 'd': self.d}
236 |
237 | @python_2_unicode_compatible
238 | class SetLiteral(Literal):
239 |
240 | def __init__(self, s=None, json_dict=None):
241 | if json_dict:
242 | self.s = json_dict['s']
243 | else:
244 | self.s = s
245 |
246 | def __eq__(self, other):
247 |
248 | if not isinstance(other, SetLiteral):
249 | return False
250 |
251 | return other.s == self.s
252 |
253 | def __ne__(self, other):
254 |
255 | if not isinstance(other, SetLiteral):
256 | return True
257 |
258 | return other.s != self.s
259 |
260 | def get_literal(self):
261 | return self.s
262 |
263 | def __str__(self):
264 | return text_type(self.s)
265 |
266 | def __repr__(self):
267 | return repr(self.s)
268 |
269 | def to_dict(self):
270 | return {'pt': 'SetLiteral', 's': self.s}
271 |
272 | @python_2_unicode_compatible
273 | class Variable(JSONLogic):
274 |
275 | def __init__(self, name=None, json_dict=None):
276 | if json_dict:
277 | self.name = json_dict['name']
278 | else:
279 | self.name = name
280 |
281 | def __repr__(self):
282 | return u'Variable(' + self.__unicode__() + u')'
283 |
284 | def __str__(self):
285 | return self.name
286 |
287 | def __eq__(self, other):
288 | return isinstance(other, Variable) and other.name == self.name
289 |
290 | def __hash__(self):
291 | return hash(self.name)
292 |
293 | def to_dict(self):
294 | return {'pt': 'Variable', 'name': self.name}
295 |
296 | @python_2_unicode_compatible
297 | class Predicate(JSONLogic):
298 |
299 | def __init__(self, name=None, args=None, json_dict=None):
300 |
301 | if json_dict:
302 | self.name = json_dict['name']
303 | self.args = json_dict['args']
304 |
305 | else:
306 | self.name = name
307 | self.args = args if args else []
308 |
309 | def __str__(self):
310 | if not self.args:
311 | return self.name
312 |
313 | # if self.name == 'and':
314 | # return u', '.join(map(unicode, self.args))
315 | # if self.name == 'or':
316 | # return u'; '.join(map(unicode, self.args))
317 | # elif self.name == 'and':
318 | # return u', '.join(map(unicode, self.args))
319 |
320 | return u'%s(%s)' % (self.name, u', '.join(map(text_type, self.args)))
321 | #return '(' + self.name + ' ' + ' '.join( [str(arg) for arg in self.args]) + ')'
322 |
323 | def __repr__(self):
324 | return u'Predicate(' + text_type(self) + ')'
325 |
326 | def __eq__(self, other):
327 | return isinstance(other, Predicate) \
328 | and self.name == other.name \
329 | and self.args == other.args
330 |
331 | def __ne__(self, other):
332 | if not isinstance(other, Predicate):
333 | return True
334 | if self.name != other.name:
335 | return True
336 | if self.args != other.args:
337 | return True
338 | return False
339 |
340 | def to_dict(self):
341 | return {'pt' : 'Predicate',
342 | 'name': self.name,
343 | 'args': list(map(lambda a: a.to_dict(), self.args))
344 | }
345 |
346 | def __hash__(self):
347 | # FIXME hash args?
348 | return hash(self.name + u'/' + text_type(len(self.args)))
349 |
350 | # helper function
351 |
352 | def build_predicate(name, args):
353 | mapped_args = []
354 | for arg in args:
355 | if not isinstance(arg, string_types):
356 | if isinstance (arg, int):
357 | mapped_args.append(NumberLiteral(arg))
358 | elif isinstance (arg, float):
359 | mapped_args.append(NumberLiteral(arg))
360 | else:
361 | mapped_args.append(arg)
362 | continue
363 | if arg[0].isupper() or arg[0].startswith('_'):
364 | mapped_args.append(Variable(arg))
365 | else:
366 | mapped_args.append(Predicate(arg))
367 | return Predicate (name, mapped_args)
368 |
369 | @python_2_unicode_compatible
370 | class Clause(JSONLogic):
371 |
372 | def __init__(self, head=None, body=None, location=None, json_dict=None):
373 | if json_dict:
374 | self.head = json_dict['head']
375 | self.body = json_dict['body']
376 | self.location = json_dict['location']
377 | else:
378 | self.head = head
379 | self.body = body
380 | self.location = location
381 |
382 | def __str__(self):
383 | if self.body:
384 | return u'%s :- %s.' % (text_type(self.head), text_type(self.body))
385 | return text_type(self.head) + '.'
386 |
387 | def __repr__(self):
388 | return u'Clause(' + text_type(self) + u')'
389 |
390 | def __eq__(self, other):
391 | return (isinstance(other, Clause)
392 | and self.head == other.head
393 | and list(self.body) == list(other.body))
394 |
395 | def to_dict(self):
396 | return {'pt' : 'Clause',
397 | 'head' : self.head.to_dict(),
398 | 'body' : self.body.to_dict() if self.body else None,
399 | 'location': self.location.to_dict(),
400 | }
401 |
402 | @python_2_unicode_compatible
403 | class MacroCall(JSONLogic):
404 |
405 | def __init__(self, name=None, pred=None, location=None, json_dict=None):
406 | if json_dict:
407 | self.name = json_dict['name']
408 | self.pred = json_dict['pred']
409 | self.location = json_dict['location']
410 | else:
411 | self.name = name
412 | self.pred = pred
413 | self.location = location
414 |
415 | def __str__(self):
416 | return u'@' + text_type(self.name) + u':' + text_type(self.pred)
417 |
418 | def __repr__(self):
419 | return 'MacroCall(%s, %s)' % (self.name, self.pred)
420 |
421 | def to_dict(self):
422 | return {'pt' : 'MacroCall',
423 | 'name' : self.name,
424 | 'pred' : self.pred,
425 | 'location': self.location.to_dict(),
426 | }
427 | #
428 | # JSON interface
429 | #
430 |
431 | class PrologJSONEncoder(json.JSONEncoder):
432 |
433 | def default(self, o):
434 |
435 | if isinstance (o, JSONLogic):
436 | return o.to_dict()
437 |
438 | try:
439 | return json.JSONEncoder.default(self, o)
440 | except TypeError:
441 | import pdb; pdb.set_trace()
442 |
443 |
444 | _prolog_json_encoder = PrologJSONEncoder()
445 |
446 | def prolog_to_json(pl):
447 | return _prolog_json_encoder.encode(pl)
448 |
449 | def _prolog_from_json(o):
450 |
451 | if o == None:
452 | return None
453 |
454 | if not 'pt' in o:
455 | # import pdb; pdb.set_trace()
456 | # raise PrologError('cannot convert from json: %s [pt missing] .' % repr(o))
457 | return o
458 |
459 | if o['pt'] == 'Clause':
460 | return Clause(json_dict=o)
461 | if o['pt'] == 'Predicate':
462 | return Predicate(json_dict=o)
463 | if o['pt'] == 'StringLiteral':
464 | return StringLiteral (json_dict=o)
465 | if o['pt'] == 'NumberLiteral':
466 | return NumberLiteral (json_dict=o)
467 | if o['pt'] == 'ListLiteral':
468 | return ListLiteral (json_dict=o)
469 | if o['pt'] == 'DictLiteral':
470 | return DictLiteral (json_dict=o)
471 | if o['pt'] == 'SetLiteral':
472 | return SetLiteral (json_dict=o)
473 | if o['pt'] == 'Variable':
474 | return Variable (json_dict=o)
475 | if o['pt'] == 'SourceLocation':
476 | return SourceLocation (json_dict=o)
477 | if o['pt'] == 'MacroCall':
478 | return MacroCall (json_dict=o)
479 |
480 | raise PrologError('cannot convert from json: %s .' % repr(o))
481 |
482 | def json_to_prolog(jstr):
483 | return json.JSONDecoder(object_hook = _prolog_from_json).decode(jstr)
484 |
485 |
--------------------------------------------------------------------------------
/zamiaprolog/logicdb.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # store and retrieve logic clauses to and from our relational db
22 | #
23 |
24 | import os
25 | import sys
26 | import logging
27 | import time
28 |
29 | from copy import deepcopy, copy
30 | from sqlalchemy import create_engine
31 | from sqlalchemy.orm import sessionmaker
32 | from six import python_2_unicode_compatible, text_type
33 | from zamiaprolog import model
34 |
35 | from zamiaprolog.logic import *
36 | from nltools.misc import limit_str
37 |
38 | class LogicDB(object):
39 |
40 | def __init__(self, db_url, echo=False):
41 |
42 | self.engine = create_engine(db_url, echo=echo)
43 | self.Session = sessionmaker(bind=self.engine)
44 | self.session = self.Session()
45 | model.Base.metadata.create_all(self.engine)
46 | self.cache = {}
47 |
48 | def commit(self):
49 | logging.debug("commit.")
50 | self.session.commit()
51 |
52 | def close (self, do_commit=True):
53 | if do_commit:
54 | self.commit()
55 | self.session.close()
56 |
57 | def clear_module(self, module, commit=True):
58 |
59 | logging.info("Clearing %s ..." % module)
60 | self.session.query(model.ORMClause).filter(model.ORMClause.module==module).delete()
61 | self.session.query(model.ORMPredicateDoc).filter(model.ORMPredicateDoc.module==module).delete()
62 | logging.info("Clearing %s ... done." % module)
63 |
64 | if commit:
65 | self.commit()
66 | self.invalidate_cache()
67 |
68 | def clear_all_modules(self, commit=True):
69 |
70 | logging.info("Clearing all modules ...")
71 | self.session.query(model.ORMClause).delete()
72 | self.session.query(model.ORMPredicateDoc).delete()
73 | logging.info("Clearing all modules ... done.")
74 |
75 | if commit:
76 | self.commit()
77 | self.invalidate_cache()
78 |
79 | def store (self, module, clause):
80 |
81 | ormc = model.ORMClause(module = module,
82 | arity = len(clause.head.args),
83 | head = clause.head.name,
84 | prolog = prolog_to_json(clause))
85 |
86 | # print text_type(clause)
87 |
88 | self.session.add(ormc)
89 | self.invalidate_cache(clause.head.name)
90 |
91 | def invalidate_cache(self, name=None):
92 | if name and name in self.cache:
93 | del self.cache[name]
94 | else:
95 | self.cache = {}
96 |
97 | def store_doc (self, module, name, doc):
98 |
99 | ormd = model.ORMPredicateDoc(module = module,
100 | name = name,
101 | doc = doc)
102 | self.session.add(ormd)
103 |
104 | # use arity=-1 to disable filtering
105 | def lookup (self, name, arity, overlay=None, sf=None):
106 |
107 | ts_start = time.time()
108 |
109 | # if name == 'lang':
110 | # import pdb; pdb.set_trace()
111 |
112 | # DB caching
113 |
114 | if name in self.cache:
115 | res = copy(self.cache[name])
116 |
117 | else:
118 | res = []
119 |
120 | for ormc in self.session.query(model.ORMClause).filter(model.ORMClause.head==name).order_by(model.ORMClause.id).all():
121 |
122 | res.append (json_to_prolog(ormc.prolog))
123 |
124 | self.cache[name] = copy(res)
125 |
126 | if overlay:
127 | res = overlay.do_filter(name, res)
128 |
129 | if arity<0:
130 | return res
131 |
132 | res2 = []
133 | for clause in res:
134 |
135 | if len(clause.head.args) != arity:
136 | continue
137 |
138 | match = True
139 | if sf:
140 | for i in sf:
141 | ca = sf[i]
142 | a = clause.head.args[i]
143 |
144 | if not isinstance(a, Predicate):
145 | continue
146 | if (a.name != ca) or (len(a.args) !=0):
147 | # logging.info('no match: %s vs %s %s' % (repr(ca), repr(a), text_type(clause)))
148 | match=False
149 | break
150 | if not match:
151 | continue
152 | res2.append(clause)
153 |
154 | ts_delay = time.time() - ts_start
155 | # logging.debug (u'db lookup for %s/%d took %fs' % (name, arity, ts_delay))
156 |
157 | return res2
158 |
159 | @python_2_unicode_compatible
160 | class LogicDBOverlay(object):
161 |
162 | def __init__(self):
163 |
164 | self.d_assertz = {}
165 | self.d_retracted = {}
166 |
167 | def clone(self):
168 | clone = LogicDBOverlay()
169 |
170 | for name in self.d_retracted:
171 | for c in self.d_retracted[name]:
172 | clone.retract(c)
173 |
174 | for name in self.d_assertz:
175 | for c in self.d_assertz[name]:
176 | clone.assertz(c)
177 |
178 | return clone
179 |
180 | def assertz (self, clause):
181 |
182 | name = clause.head.name
183 |
184 | if name in self.d_assertz:
185 | self.d_assertz[name].append(clause)
186 | else:
187 | self.d_assertz[name] = [clause]
188 |
189 | def _match_p (self, p1, p2):
190 |
191 | """ extremely simplified variant of full-blown unification - just enough to get basic retract/1 working """
192 |
193 | if isinstance (p1, Variable):
194 | return True
195 |
196 | if isinstance (p2, Variable):
197 | return True
198 |
199 | elif isinstance (p1, Literal):
200 | return p1 == p2
201 |
202 | elif p1.name != p2.name:
203 | return False
204 |
205 | elif len(p1.args) != len(p2.args):
206 | return False
207 |
208 | else:
209 | for i in range(len(p1.args)):
210 | if not self._match_p(p1.args[i], p2.args[i]):
211 | return False
212 |
213 | return True
214 |
215 |
216 | def retract (self, p):
217 | name = p.name
218 |
219 | if name in self.d_assertz:
220 | l = []
221 | for c in self.d_assertz[name]:
222 | if not self._match_p(p, c.head):
223 | l.append(c)
224 | self.d_assertz[name] = l
225 |
226 | if name in self.d_retracted:
227 | self.d_retracted[name].append(p)
228 | else:
229 | self.d_retracted[name] = [p]
230 |
231 | def do_filter (self, name, res):
232 |
233 | if name in self.d_retracted:
234 | res2 = []
235 | for clause in res:
236 | for p in self.d_retracted[name]:
237 | if not self._match_p(clause.head, p):
238 | res2.append(clause)
239 | res = res2
240 |
241 | # append overlay clauses
242 |
243 | if name in self.d_assertz:
244 | for clause in self.d_assertz[name]:
245 | res.append(clause)
246 |
247 | return res
248 |
249 | def log_trace (self, indent):
250 | for k in sorted(self.d_assertz):
251 | for clause in self.d_assertz[k]:
252 | logging.info(u"%s [O] %s" % (indent, limit_str(text_type(clause), 100)))
253 | # FIXME: log retracted clauses?
254 |
255 | def __str__ (self):
256 | res = u'DBOvl('
257 | for k in sorted(self.d_assertz):
258 | for clause in self.d_assertz[k]:
259 | res += u'+' + limit_str(text_type(clause), 40)
260 | for k in sorted(self.d_retracted):
261 | for p in self.d_retracted[k]:
262 | res += u'-' + limit_str(text_type(p), 40)
263 |
264 | res += u')'
265 | return res
266 |
267 | def __repr__(self):
268 | return text_type(self).encode('utf8')
269 |
270 | def do_apply(self, module, db, commit=True):
271 |
272 | to_delete = set()
273 |
274 | for name in self.d_retracted:
275 | for ormc in db.session.query(model.ORMClause).filter(model.ORMClause.head==name).all():
276 | clause = json_to_prolog(ormc.prolog)
277 | for p in self.d_retracted[name]:
278 | if self._match_p(clause.head, p):
279 | to_delete.add(ormc.id)
280 |
281 | if to_delete:
282 | db.session.query(model.ORMClause).filter(model.ORMClause.id.in_(list(to_delete))).delete(synchronize_session='fetch')
283 | db.invalidate_cache()
284 |
285 | for name in self.d_assertz:
286 | for clause in self.d_assertz[name]:
287 | db.store(module, clause)
288 |
289 | if commit:
290 | db.commit()
291 |
292 |
293 | # class LogicMemDB(object):
294 | #
295 | # def __init__(self):
296 | # self.clauses = {}
297 | #
298 | # def store (self, clause):
299 | # if clause.head.name in self.clauses:
300 | # self.clauses[clause.head.name].append (clause)
301 | # else:
302 | # self.clauses[clause.head.name] = [clause]
303 | #
304 | # def lookup (self, name):
305 | # if name in self.clauses:
306 | # return self.clauses[name]
307 | # return []
308 |
309 |
--------------------------------------------------------------------------------
/zamiaprolog/model.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | import sys
22 |
23 | from sqlalchemy import Column, Integer, String, Text, Unicode, UnicodeText, Enum, DateTime, ForeignKey
24 | from sqlalchemy.orm import relationship
25 | from sqlalchemy.ext.declarative import declarative_base
26 |
27 | from nltools import misc
28 |
29 | Base = declarative_base()
30 |
31 | class ORMClause(Base):
32 |
33 | __tablename__ = 'clauses'
34 |
35 | id = Column(Integer, primary_key=True)
36 |
37 | module = Column(String(255), index=True)
38 | head = Column(String(255), index=True)
39 | arity = Column(Integer, index=True)
40 | prolog = Column(Text)
41 |
42 | class ORMPredicateDoc(Base):
43 |
44 | __tablename__ = 'predicate_docs'
45 |
46 | module = Column(String(255), index=True)
47 | name = Column(String(255), primary_key=True)
48 |
49 | doc = Column(UnicodeText)
50 |
51 | class ORMGensymNum(Base):
52 |
53 | __tablename__ = 'gensym_nums'
54 |
55 | root = Column(String(255), primary_key=True)
56 | current_num = Column(Integer)
57 |
58 |
--------------------------------------------------------------------------------
/zamiaprolog/parser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # Zamia-Prolog
22 | # ------------
23 | #
24 | # Zamia-Prolog grammar
25 | #
26 | # program ::= { clause }
27 | #
28 | # clause ::= relation [ ':-' clause_body ] '.'
29 | #
30 | # relation ::= name [ '(' term { ',' term } ')' ]
31 | #
32 | # clause_body ::= subgoals { ';' subgoals }
33 | #
34 | # subgoals ::= subgoal { ',' subgoal }
35 | #
36 | # subgoal ::= ( term | conditional | inline )
37 | #
38 | # inline ::= 'inline' relation
39 | #
40 | # conditional ::= 'if' term 'then' subgoals [ 'else' subgoals ] 'endif'
41 | #
42 | # term ::= add-term { rel-op add-term }
43 | #
44 | # rel-op ::= '=' | '\=' | '<' | '>' | '=<' | '>=' | 'is' | ':='
45 | #
46 | # add-term ::= mul-term { add-op mul-term }
47 | #
48 | # add-op ::= '+' | '-'
49 | #
50 | # mul-term ::= unary-term { mul-op unary-term }
51 | #
52 | # mul-op ::= '*' | '/' | 'div' | 'mod'
53 | #
54 | # unary-term ::= [ unary-op ] primary-term
55 | #
56 | # unary-op ::= '+' | '-'
57 | #
58 | # primary-term ::= ( variable | number | string | list | relation | '(' term { ',' term } ')' | '!' )
59 | #
60 | # list ::= '[' [ primary-term ] ( { ',' primary-term } | '|' primary-term ) ']'
61 | #
62 |
63 | import os
64 | import sys
65 | import logging
66 | import codecs
67 | import re
68 |
69 | from copy import copy
70 |
71 | from six import StringIO, text_type
72 |
73 | from zamiaprolog.logic import *
74 | from zamiaprolog.errors import *
75 | from zamiaprolog.runtime import PrologRuntime
76 | from nltools.tokenizer import tokenize
77 |
78 | # lexer
79 |
80 | NAME_CHARS = set([u'a',u'b',u'c',u'd',u'e',u'f',u'g',u'h',u'i',u'j',u'k',u'l',u'm',u'n',u'o',u'p',u'q',u'r',u's',u't',u'u',u'v',u'w',u'x',u'y',u'z',
81 | u'A',u'B',u'C',u'D',u'E',u'F',u'G',u'H',u'I',u'J',u'K',u'L',u'M',u'N',u'O',u'P',u'Q',u'R',u'S',u'T',u'U',u'V',u'W',u'X',u'Y',u'Z',
82 | u'_',u'0',u'1',u'2',u'3',u'4',u'5',u'6',u'7',u'8',u'9'])
83 |
84 | NAME_CHARS_EXTENDED = NAME_CHARS | set([':','|'])
85 |
86 |
87 | SYM_NONE = 0
88 | SYM_EOF = 1
89 | SYM_STRING = 2 # 'abc'
90 | SYM_NAME = 3 # abc aWord =< is div +
91 | SYM_VARIABLE = 4 # X Variable _Variable _
92 | SYM_NUMBER = 5
93 |
94 | SYM_IMPL = 10 # :-
95 | SYM_LPAREN = 11 # (
96 | SYM_RPAREN = 12 # )
97 | SYM_COMMA = 13 # ,
98 | SYM_PERIOD = 14 # .
99 | SYM_SEMICOLON = 15 # ;
100 | SYM_COLON = 17 # :
101 | SYM_LBRACKET = 18 # [
102 | SYM_RBRACKET = 19 # ]
103 | SYM_PIPE = 20 # |
104 | SYM_CUT = 21 # !
105 |
106 | SYM_EQUAL = 22 # =
107 | SYM_NEQUAL = 23 # \=, !=
108 | SYM_LESS = 24 # <
109 | SYM_GREATER = 25 # >
110 | SYM_LESSEQ = 26 # =<, <=
111 | SYM_GREATEREQ = 27 # >=
112 | SYM_IS = 28 # is
113 | SYM_SET = 29 # set, :=
114 |
115 | SYM_PLUS = 30 # +
116 | SYM_MINUS = 31 # -
117 | SYM_ASTERISK = 32 # *
118 | SYM_DIV = 33 # div, /
119 | SYM_MOD = 34 # mod
120 |
121 | SYM_IF = 40 # if
122 | SYM_THEN = 41 # then
123 | SYM_ELSE = 42 # else
124 | SYM_ENDIF = 43 # endif
125 | SYM_INLINE = 44 # inline
126 |
127 | REL_OPS = set([SYM_EQUAL,
128 | SYM_NEQUAL,
129 | SYM_LESS,
130 | SYM_GREATER,
131 | SYM_LESSEQ,
132 | SYM_GREATEREQ,
133 | SYM_IS,
134 | SYM_SET])
135 |
136 | UNARY_OPS = set([SYM_PLUS, SYM_MINUS])
137 | ADD_OPS = set([SYM_PLUS, SYM_MINUS])
138 | MUL_OPS = set([SYM_ASTERISK, SYM_DIV, SYM_MOD])
139 |
140 | REL_NAMES = {
141 | SYM_EQUAL : u'=',
142 | SYM_NEQUAL : u'\\=',
143 | SYM_LESS : u'<',
144 | SYM_GREATER : u'>',
145 | SYM_LESSEQ : u'=<',
146 | SYM_GREATEREQ : u'>=',
147 | SYM_IS : u'is',
148 | SYM_SET : u'set',
149 | SYM_PLUS : u'+',
150 | SYM_MINUS : u'-',
151 | SYM_ASTERISK : u'*',
152 | SYM_DIV : u'/',
153 | SYM_MOD : u'mod',
154 | SYM_CUT : u'cut',
155 | }
156 |
157 |
158 |
159 | # structured comments
160 | CSTATE_IDLE = 0
161 | CSTATE_HEADER = 1
162 | CSTATE_BODY = 2
163 |
164 | class PrologParser(object):
165 |
166 | def __init__(self, db, do_inline = True):
167 | # compile-time built-in predicates
168 | self.directives = {}
169 | self.db = db
170 | self.do_inline = do_inline
171 |
172 | def report_error(self, s):
173 | raise PrologError ("%s: error in line %d col %d: %s" % (self.prolog_fn, self.cur_line, self.cur_col, s))
174 |
175 | def get_location(self):
176 | return SourceLocation(self.prolog_fn, self.cur_line, self.cur_col)
177 |
178 | def next_c(self):
179 | self.cur_c = text_type(self.prolog_f.read(1))
180 | self.cur_col += 1
181 |
182 | if self.cur_c == u'\n':
183 | self.cur_line += 1
184 | self.cur_col = 1
185 | if (self.linecnt > 0) and (self.cur_line % 1000 == 0):
186 | logging.info ("%s: parsing line %6d / %6d (%3d%%)" % (self.prolog_fn,
187 | self.cur_line,
188 | self.linecnt,
189 | self.cur_line * 100 / self.linecnt))
190 |
191 | # print '[', self.cur_c, ']',
192 |
193 | def peek_c(self):
194 | peek_c = text_type(self.prolog_f.read(1))
195 | self.prolog_f.seek(-1,1)
196 | return peek_c
197 |
198 | def is_name_char(self, c):
199 | if c in NAME_CHARS:
200 | return True
201 |
202 | if ord(c) >= 128:
203 | return True
204 |
205 | return False
206 |
207 | def is_name_char_ext(self, c):
208 | if c in NAME_CHARS_EXTENDED:
209 | return True
210 |
211 | if ord(c) >= 128:
212 | return True
213 |
214 | return False
215 |
216 | def next_sym(self):
217 |
218 | # whitespace, comments
219 |
220 | self.cstate = CSTATE_IDLE
221 |
222 | while True:
223 | # skip whitespace
224 | while not (self.cur_c is None) and self.cur_c.isspace():
225 | self.next_c()
226 |
227 | if not self.cur_c:
228 | self.cur_sym = SYM_EOF
229 | return
230 |
231 | # skip comments
232 | if self.cur_c == u'%':
233 |
234 | comment_line = u''
235 |
236 | self.next_c()
237 | if self.cur_c == u'!':
238 | self.cstate = CSTATE_HEADER
239 | self.next_c()
240 |
241 | while True:
242 | if not self.cur_c:
243 | self.cur_sym = SYM_EOF
244 | return
245 | if self.cur_c == u'\n':
246 | self.next_c()
247 | break
248 | comment_line += self.cur_c
249 | self.next_c()
250 |
251 | if self.cstate == CSTATE_HEADER:
252 | m = re.match (r"^\s*doc\s+([a-zA-Z0-9_]+)", comment_line)
253 | if m:
254 | self.comment_pred = m.group(1)
255 | self.comment = ''
256 | self.cstate = CSTATE_BODY
257 |
258 | elif self.cstate == CSTATE_BODY:
259 | if len(self.comment)>0:
260 | self.comment += '\n'
261 | self.comment += comment_line.lstrip().rstrip()
262 |
263 | else:
264 | break
265 |
266 | #if self.comment_pred:
267 | # print "COMMENT FOR %s : %s" % (self.comment_pred, self.comment)
268 |
269 | self.cur_str = u''
270 |
271 | # import pdb; pdb.set_trace()
272 |
273 | if self.cur_c == u'\'' or self.cur_c == u'"':
274 | self.cur_sym = SYM_STRING
275 | startc = self.cur_c
276 |
277 | while True:
278 | self.next_c()
279 |
280 | if not self.cur_c:
281 | self.report_error ("Unterminated string literal.")
282 | self.cur_sym = SYM_EOF
283 | break
284 | if self.cur_c == u'\\':
285 | self.next_c()
286 | self.cur_str += self.cur_c
287 | self.next_c()
288 |
289 | if self.cur_c == startc:
290 | self.next_c()
291 | break
292 |
293 | self.cur_str += self.cur_c
294 |
295 | elif self.cur_c.isdigit():
296 | self.cur_sym = SYM_NUMBER
297 |
298 | while True:
299 | self.cur_str += self.cur_c
300 | self.next_c()
301 | if self.cur_c == '.' and not self.peek_c().isdigit():
302 | break
303 |
304 | if not self.cur_c or (not self.cur_c.isdigit() and self.cur_c != '.'):
305 | break
306 |
307 | elif self.is_name_char(self.cur_c):
308 | self.cur_sym = SYM_VARIABLE if self.cur_c == u'_' or self.cur_c.isupper() else SYM_NAME
309 |
310 | while True:
311 | self.cur_str += self.cur_c
312 | self.next_c()
313 | if not self.cur_c or not self.is_name_char_ext(self.cur_c):
314 | break
315 |
316 | # keywords
317 |
318 | if self.cur_str == 'if':
319 | self.cur_sym = SYM_IF
320 | elif self.cur_str == 'then':
321 | self.cur_sym = SYM_THEN
322 | elif self.cur_str == 'else':
323 | self.cur_sym = SYM_ELSE
324 | elif self.cur_str == 'endif':
325 | self.cur_sym = SYM_ENDIF
326 | elif self.cur_str == 'is':
327 | self.cur_sym = SYM_IS
328 | elif self.cur_str == 'set':
329 | self.cur_sym = SYM_SET
330 | elif self.cur_str == 'div':
331 | self.cur_sym = SYM_DIV
332 | elif self.cur_str == 'mod':
333 | self.cur_sym = SYM_MOD
334 | elif self.cur_str == 'inline':
335 | self.cur_sym = SYM_INLINE
336 |
337 | elif self.cur_c == u':':
338 | self.next_c()
339 |
340 | if self.cur_c == u'-':
341 | self.next_c()
342 | self.cur_sym = SYM_IMPL
343 | elif self.cur_c == u'=':
344 | self.next_c()
345 | self.cur_sym = SYM_SET
346 | else:
347 | self.cur_sym = SYM_COLON
348 |
349 | elif self.cur_c == u'(':
350 | self.cur_sym = SYM_LPAREN
351 | self.next_c()
352 | elif self.cur_c == u')':
353 | self.cur_sym = SYM_RPAREN
354 | self.next_c()
355 |
356 | elif self.cur_c == u',':
357 | self.cur_sym = SYM_COMMA
358 | self.next_c()
359 |
360 | elif self.cur_c == u'.':
361 | self.cur_sym = SYM_PERIOD
362 | self.next_c()
363 |
364 | elif self.cur_c == u';':
365 | self.cur_sym = SYM_SEMICOLON
366 | self.next_c()
367 |
368 | elif self.cur_c == u'[':
369 | self.cur_sym = SYM_LBRACKET
370 | self.next_c()
371 |
372 | elif self.cur_c == u']':
373 | self.cur_sym = SYM_RBRACKET
374 | self.next_c()
375 |
376 | elif self.cur_c == u'|':
377 | self.cur_sym = SYM_PIPE
378 | self.next_c()
379 |
380 | elif self.cur_c == u'+':
381 | self.cur_sym = SYM_PLUS
382 | self.next_c()
383 |
384 | elif self.cur_c == u'-':
385 | self.cur_sym = SYM_MINUS
386 | self.next_c()
387 |
388 | elif self.cur_c == u'*':
389 | self.cur_sym = SYM_ASTERISK
390 | self.next_c()
391 |
392 | elif self.cur_c == u'/':
393 | self.cur_sym = SYM_DIV
394 | self.next_c()
395 |
396 | elif self.cur_c == u'!':
397 | self.next_c()
398 |
399 | if self.cur_c == u'=':
400 | self.next_c()
401 | self.cur_sym = SYM_NEQUAL
402 | else:
403 | self.cur_sym = SYM_CUT
404 |
405 | elif self.cur_c == u'=':
406 | self.next_c()
407 |
408 | if self.cur_c == u'<':
409 | self.next_c()
410 | self.cur_sym = SYM_LESSEQ
411 | else:
412 | self.cur_sym = SYM_EQUAL
413 |
414 | elif self.cur_c == u'<':
415 | self.next_c()
416 |
417 | if self.cur_c == u'=':
418 | self.next_c()
419 | self.cur_sym = SYM_LESSEQ
420 | else:
421 | self.cur_sym = SYM_LESS
422 |
423 | elif self.cur_c == u'>':
424 | self.next_c()
425 |
426 | if self.cur_c == u'=':
427 | self.next_c()
428 | self.cur_sym = SYM_GREATEREQ
429 | else:
430 | self.cur_sym = SYM_GREATER
431 |
432 | elif self.cur_c == u'\\':
433 | self.next_c()
434 |
435 | if self.cur_c == u'=':
436 | self.next_c()
437 | self.cur_sym = SYM_NEQUAL
438 | else:
439 | self.report_error ("Lexer error: \\= expected")
440 |
441 | else:
442 | self.report_error ("Illegal character: " + repr(self.cur_c))
443 |
444 | # logging.info( "[%2d]" % self.cur_sym )
445 |
446 |
447 | #
448 | # parser starts here
449 | #
450 |
451 | def parse_list(self):
452 |
453 | res = ListLiteral([])
454 |
455 | if self.cur_sym != SYM_RBRACKET:
456 |
457 | res.l.append(self.primary_term())
458 |
459 | # FIXME: implement proper head/tail mechanics
460 |
461 | if self.cur_sym == SYM_PIPE:
462 | self.next_sym()
463 | res.l.append(self.primary_term())
464 |
465 | else:
466 |
467 | while (self.cur_sym == SYM_COMMA):
468 | self.next_sym()
469 | res.l.append(self.primary_term())
470 |
471 | if self.cur_sym != SYM_RBRACKET:
472 | self.report_error ("list: ] expected.")
473 | self.next_sym()
474 |
475 | return res
476 |
477 | def primary_term(self):
478 |
479 | res = None
480 |
481 | if self.cur_sym == SYM_VARIABLE:
482 | res = Variable (self.cur_str)
483 | self.next_sym()
484 |
485 | elif self.cur_sym == SYM_NUMBER:
486 | res = NumberLiteral (float(self.cur_str))
487 | self.next_sym()
488 |
489 | elif self.cur_sym == SYM_STRING:
490 | res = StringLiteral (self.cur_str)
491 | self.next_sym()
492 |
493 | elif self.cur_sym == SYM_NAME:
494 | res = self.relation()
495 | elif self.cur_sym in REL_NAMES:
496 | res = self.relation()
497 |
498 | elif self.cur_sym == SYM_LPAREN:
499 | self.next_sym()
500 | res = self.term()
501 |
502 | while (self.cur_sym == SYM_COMMA):
503 | self.next_sym()
504 | if not isinstance(res, list):
505 | res = [res]
506 | res.append(self.term())
507 |
508 | if self.cur_sym != SYM_RPAREN:
509 | self.report_error ("primary term: ) expected.")
510 | self.next_sym()
511 |
512 | elif self.cur_sym == SYM_LBRACKET:
513 | self.next_sym()
514 | res = self.parse_list()
515 |
516 | else:
517 | self.report_error ("primary term: variable / number / string / name / ( expected, sym #%d found instead." % self.cur_sym)
518 |
519 | # logging.debug ('primary_term: %s' % str(res))
520 |
521 | return res
522 |
523 | def unary_term(self):
524 |
525 | o = None
526 |
527 | if self.cur_sym in UNARY_OPS:
528 | o = REL_NAMES[self.cur_sym]
529 | self.next_sym()
530 |
531 | res = self.primary_term()
532 | if o:
533 | if not isinstance(res, list):
534 | res = [res]
535 |
536 | res = Predicate (o, res)
537 |
538 | return res
539 |
540 |
541 | def mul_term(self):
542 |
543 | args = []
544 | ops = []
545 |
546 | args.append(self.unary_term())
547 |
548 | while self.cur_sym in MUL_OPS:
549 | o = REL_NAMES[self.cur_sym]
550 | ops.append(o)
551 | self.next_sym()
552 | args.append(self.unary_term())
553 |
554 | res = None
555 | while len(args)>0:
556 | arg = args.pop()
557 | if not res:
558 | res = arg
559 | else:
560 | res = Predicate (o, [arg, res])
561 |
562 | if len(ops)>0:
563 | o = ops.pop()
564 |
565 | # logging.debug ('mul_term: ' + str(res))
566 |
567 | return res
568 |
569 |
570 | def add_term(self):
571 |
572 | args = []
573 | ops = []
574 |
575 | args.append(self.mul_term())
576 |
577 | while self.cur_sym in ADD_OPS:
578 | o = REL_NAMES[self.cur_sym]
579 | ops.append(o)
580 | self.next_sym()
581 | args.append(self.mul_term())
582 |
583 | res = None
584 | while len(args)>0:
585 | arg = args.pop()
586 | if not res:
587 | res = arg
588 | else:
589 | res = Predicate (o, [arg, res])
590 |
591 | if len(ops)>0:
592 | o = ops.pop()
593 |
594 | # logging.debug ('add_term: ' + str(res))
595 |
596 | return res
597 |
598 | def term(self):
599 |
600 | args = []
601 | ops = []
602 |
603 | args.append(self.add_term())
604 |
605 | while self.cur_sym in REL_OPS:
606 | ops.append(REL_NAMES[self.cur_sym])
607 | self.next_sym()
608 | args.append(self.add_term())
609 |
610 | res = None
611 | while len(args)>0:
612 | arg = args.pop()
613 | if not res:
614 | res = arg
615 | else:
616 | res = Predicate (o, [arg, res])
617 |
618 | if len(ops)>0:
619 | o = ops.pop()
620 |
621 | # logging.debug ('term: ' + str(res))
622 |
623 | return res
624 |
625 |
626 | def relation(self):
627 |
628 | if self.cur_sym in REL_NAMES:
629 | name = REL_NAMES[self.cur_sym]
630 | elif self.cur_sym == SYM_NAME:
631 | name = self.cur_str
632 | else:
633 | self.report_error ("Name expected.")
634 | self.next_sym()
635 |
636 | args = None
637 |
638 | if self.cur_sym == SYM_LPAREN:
639 | self.next_sym()
640 |
641 | args = []
642 |
643 | while True:
644 |
645 | args.append(self.term())
646 |
647 | if self.cur_sym != SYM_COMMA:
648 | break
649 | self.next_sym()
650 |
651 | if self.cur_sym != SYM_RPAREN:
652 | self.report_error ("relation: ) expected.")
653 | self.next_sym()
654 |
655 | return Predicate (name, args)
656 |
657 | def _apply_bindings (self, a, bindings):
658 | """ static application of bindings when inlining predicates """
659 |
660 | if isinstance (a, Predicate):
661 |
662 | aargs = []
663 | for b in a.args:
664 | aargs.append(self._apply_bindings (b, bindings))
665 |
666 | return Predicate (a.name, aargs)
667 |
668 | if isinstance (a, Variable):
669 | if not a.name in bindings:
670 | return a
671 | return bindings[a.name]
672 |
673 | if isinstance (a, ListLiteral):
674 | rl = []
675 | for i in a.l:
676 | rl.append(self._apply_bindings (i, bindings))
677 | return ListLiteral(rl)
678 |
679 | if isinstance (a, Literal):
680 | return a
681 |
682 | raise Exception ('_apply_bindings not implemented yet for %s' % a.__class__)
683 |
684 | def subgoal(self):
685 |
686 | if self.cur_sym == SYM_IF:
687 |
688 | self.next_sym()
689 |
690 | c = self.term()
691 |
692 | if self.cur_sym != SYM_THEN:
693 | self.report_error ("subgoal: then expected.")
694 | self.next_sym()
695 |
696 | t = self.subgoals()
697 | t = Predicate ('and', [c, t])
698 |
699 | if self.cur_sym == SYM_ELSE:
700 | self.next_sym()
701 | e = self.subgoals()
702 | else:
703 | e = Predicate ('true')
704 |
705 | nc = Predicate ('not', [c])
706 | e = Predicate ('and', [nc, e])
707 |
708 | if self.cur_sym != SYM_ENDIF:
709 | self.report_error ("subgoal: endif expected.")
710 | self.next_sym()
711 |
712 | return [ Predicate ('or', [t, e]) ]
713 |
714 | elif self.cur_sym == SYM_INLINE:
715 |
716 | self.next_sym()
717 |
718 | pred = self.relation()
719 |
720 | if not self.do_inline:
721 | return [ Predicate ('inline', [pred]) ]
722 |
723 | # if self.cur_line == 38:
724 | # import pdb; pdb.set_trace()
725 |
726 | # see if we can find a clause that unifies with the pred to inline
727 |
728 | clauses = self.db.lookup(pred.name, arity=-1)
729 | succeeded = None
730 | succ_bind = None
731 | for clause in reversed(clauses):
732 |
733 | if len(clause.head.args) != len(pred.args):
734 | continue
735 |
736 | bindings = {}
737 | if self.rt._unify (pred, {}, clause.head, bindings, clause.location, overwrite_vars = False):
738 | if succeeded:
739 | self.report_error ("inline: %s: more than one matching pred found." % text_type(pred))
740 |
741 | succeeded = clause
742 | succ_bind = bindings
743 |
744 | if not succeeded:
745 | self.report_error ("inline: %s: no matching pred found." % text_type(pred))
746 |
747 | res = []
748 |
749 | if isinstance(succeeded.body, Predicate):
750 | if succeeded.body.name == 'and':
751 | for a in succeeded.body.args:
752 | res.append(self._apply_bindings(a, succ_bind))
753 | else:
754 | res2 = []
755 | for a in succeeded.body.args:
756 | res2.append(self._apply_bindings(a, succ_bind))
757 |
758 | res.append(Predicate (succeeded.body.name, res2))
759 |
760 | elif isinstance(succeeded.body, StringLiteral):
761 | res.append(self._apply_bindings(succeeded.body, succ_bind))
762 |
763 | else:
764 | self.report_error ("inline: inlined predicate has wrong form.")
765 |
766 | return res
767 |
768 | else:
769 | return [ self.term() ]
770 |
771 | def subgoals(self):
772 |
773 | res = self.subgoal()
774 |
775 | while self.cur_sym == SYM_COMMA:
776 | self.next_sym()
777 |
778 | t2 = self.subgoal()
779 | res.extend(t2)
780 |
781 | if len(res) == 1:
782 | return res[0]
783 |
784 | return Predicate ('and', res)
785 |
786 | def clause_body(self):
787 |
788 | res = [ self.subgoals() ]
789 |
790 | while self.cur_sym == SYM_SEMICOLON:
791 | self.next_sym()
792 |
793 | sg2 = self.subgoals()
794 | res.append(sg2)
795 |
796 | if len(res) == 1:
797 | return res[0]
798 |
799 | return Predicate ('or', res)
800 |
801 | def clause(self):
802 |
803 | res = []
804 |
805 | loc = self.get_location()
806 |
807 | head = self.relation()
808 |
809 | if self.cur_sym == SYM_IMPL:
810 | self.next_sym()
811 |
812 | body = self.clause_body()
813 |
814 | c = Clause (head, body, location=loc)
815 |
816 | else:
817 | c = Clause (head, location=loc)
818 |
819 | if self.cur_sym != SYM_PERIOD:
820 | self.report_error ("clause: . expected.")
821 | self.next_sym()
822 |
823 | # compiler directive?
824 |
825 | if c.head.name in self.directives:
826 | f, user_data = self.directives[c.head.name]
827 | f(self.db, self.module_name, c, user_data)
828 |
829 | else:
830 | res.append(c)
831 |
832 |
833 | # logging.debug ('clause: ' + str(res))
834 |
835 | return res
836 |
837 | #
838 | # high-level interface
839 | #
840 |
841 | def start (self, prolog_f, prolog_fn, linecnt = 1, module_name = None):
842 |
843 | self.cur_c = u' '
844 | self.cur_sym = SYM_NONE
845 | self.cur_str = u''
846 | self.cur_line = 1
847 | self.cur_col = 1
848 | self.prolog_f = prolog_f
849 | self.prolog_fn = prolog_fn
850 | self.linecnt = linecnt
851 | self.module_name = module_name
852 |
853 | self.cstate = CSTATE_IDLE
854 | self.comment_pred = None
855 | self.comment = u''
856 |
857 | self.next_c()
858 | self.next_sym()
859 |
860 | def parse_line_clause_body (self, line):
861 |
862 | self.start (StringIO(line), '')
863 | body = self.clause_body()
864 |
865 | return Clause (None, body, location=self.get_location())
866 |
867 | def parse_line_clauses (self, line):
868 |
869 | self.start (StringIO(line), '')
870 | return self.clause()
871 |
872 | def register_directive(self, name, f, user_data):
873 | self.directives[name] = (f, user_data)
874 |
875 | def clear_module (self, module_name):
876 | self.db.clear_module(module_name)
877 |
878 | def compile_file (self, filename, module_name, clear_module=False):
879 |
880 | # quick source line count for progress output below
881 |
882 | self.linecnt = 1
883 | with codecs.open(filename, encoding='utf-8', errors='ignore', mode='r') as f:
884 | while f.readline():
885 | self.linecnt += 1
886 | logging.info("%s: %d lines." % (filename, self.linecnt))
887 |
888 | # remove old predicates of this module from db
889 | if clear_module:
890 | self.clear_module (module_name, db)
891 |
892 | # actual parsing starts here
893 |
894 | with codecs.open(filename, encoding='utf-8', errors='ignore', mode='r') as f:
895 | self.start(f, filename, module_name=module_name, linecnt=self.linecnt)
896 |
897 | while self.cur_sym != SYM_EOF:
898 | clauses = self.clause()
899 |
900 | for clause in clauses:
901 | logging.debug(u"%7d / %7d (%3d%%) > %s" % (self.cur_line, self.linecnt, self.cur_line * 100 / self.linecnt, text_type(clause)))
902 |
903 | self.db.store (module_name, clause)
904 |
905 | if self.comment_pred:
906 |
907 | self.db.store_doc (module_name, self.comment_pred, self.comment)
908 |
909 | self.comment_pred = None
910 | self.comment = ''
911 |
912 | self.db.commit()
913 |
914 | logging.info("Compilation succeeded.")
915 |
916 |
--------------------------------------------------------------------------------
/zamiaprolog/runtime.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Copyright 2015, 2016, 2017 Guenter Bartsch
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Lesser General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Lesser General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Lesser General Public License
18 | # along with this program. If not, see .
19 | #
20 | #
21 | # Zamia-Prolog engine
22 | #
23 | # based on http://openbookproject.net/py4fun/prolog/prolog3.html by Chris Meyers
24 | #
25 | # A Goal is a rule in at a certain point in its computation.
26 | # env contains binding, inx indexes the current term
27 | # being satisfied, parent is another Goal which spawned this one
28 | # and which we will unify back to when this Goal is complete.
29 |
30 | import os
31 | import sys
32 | import logging
33 | import codecs
34 | import re
35 | import copy
36 | import time
37 |
38 | from six import string_types
39 | from zamiaprolog.logic import *
40 | from zamiaprolog.builtins import *
41 | from zamiaprolog.errors import *
42 | from nltools.misc import limit_str
43 |
44 | SLOW_QUERY_TS = 1.0
45 |
46 | def prolog_unary_plus (a) : return NumberLiteral(a)
47 | def prolog_unary_minus (a) : return NumberLiteral(-a)
48 |
49 | unary_operators = {'+': prolog_unary_plus,
50 | '-': prolog_unary_minus}
51 |
52 | def prolog_binary_add (a,b) : return NumberLiteral(a + b)
53 | def prolog_binary_sub (a,b) : return NumberLiteral(a - b)
54 | def prolog_binary_mul (a,b) : return NumberLiteral(a * b)
55 | def prolog_binary_div (a,b) : return NumberLiteral(a / b)
56 | def prolog_binary_mod (a,b) : return NumberLiteral(a % b)
57 |
58 | binary_operators = {'+' : prolog_binary_add,
59 | '-' : prolog_binary_sub,
60 | '*' : prolog_binary_mul,
61 | '/' : prolog_binary_div,
62 | 'mod': prolog_binary_mod,
63 | }
64 |
65 | builtin_specials = set(['cut', 'fail', 'not', 'or', 'and', 'is', 'set'])
66 |
67 | class PrologGoal:
68 |
69 | def __init__ (self, head, terms, parent=None, env={}, negate=False, inx=0, location=None) :
70 |
71 | assert type(terms) is list
72 | assert location
73 |
74 | self.head = head
75 | self.terms = terms
76 | self.parent = parent
77 | self.env = env
78 | self.negate = negate
79 | self.inx = inx
80 | self.location = location
81 |
82 | def __unicode__ (self):
83 |
84 | res = u'!goal ' if self.negate else u'goal '
85 |
86 | if self.head:
87 | res += unicode(self.head)
88 | else:
89 | res += u'TOP'
90 | res += ' '
91 |
92 | for i, t in enumerate(self.terms):
93 | if i == self.inx:
94 | res += u"**"
95 | res += unicode(t) + u' '
96 |
97 | res += u'env=%s' % unicode(self.env)
98 |
99 | return res
100 |
101 | def __str__ (self) :
102 | return unicode(self).encode('utf8')
103 |
104 | def __repr__ (self):
105 | return 'PrologGoal(%s)' % str(self)
106 |
107 | def get_depth (self):
108 | if not self.parent:
109 | return 0
110 | return self.parent.get_depth() + 1
111 |
112 | class PrologRuntime(object):
113 |
114 | def register_builtin (self, name, builtin):
115 | self.builtins[name] = builtin
116 |
117 | def register_builtin_function (self, name, fn):
118 | self.builtin_functions[name] = fn
119 |
120 | def set_trace(self, trace):
121 | self.trace = trace
122 |
123 | def __init__(self, db):
124 | self.db = db
125 | self.builtins = {}
126 | self.builtin_functions = {}
127 | self.trace = False
128 |
129 | # arithmetic
130 |
131 | self.register_builtin('>', builtin_larger)
132 | self.register_builtin('<', builtin_smaller)
133 | self.register_builtin('=<', builtin_smaller_or_equal)
134 | self.register_builtin('>=', builtin_larger_or_equal)
135 | self.register_builtin('\\=', builtin_non_equal)
136 | self.register_builtin('=', builtin_equal)
137 |
138 | self.register_builtin('increment', builtin_increment) # increment (?V, +I)
139 | self.register_builtin('decrement', builtin_decrement) # decrement (?V, +D)
140 | self.register_builtin('between', builtin_between) # between (+Low, +High, ?Value)
141 |
142 | # strings
143 |
144 | self.register_builtin('sub_string', builtin_sub_string)
145 | self.register_builtin('str_append', builtin_str_append) # str_append (?String, +Append)
146 | self.register_builtin('atom_chars', builtin_atom_chars)
147 |
148 | # time and date
149 |
150 | self.register_builtin('date_time_stamp', builtin_date_time_stamp)
151 | self.register_builtin('stamp_date_time', builtin_stamp_date_time)
152 | self.register_builtin('get_time', builtin_get_time)
153 | self.register_builtin('day_of_the_week', builtin_day_of_the_week) # day_of_the_week (+Date,-DayOfTheWeek)
154 |
155 | # I/O
156 |
157 | self.register_builtin('write', builtin_write) # write (+Term)
158 | self.register_builtin('nl', builtin_nl) # nl
159 |
160 | # debug, tracing, control
161 |
162 | self.register_builtin('log', builtin_log) # log (+Level, +Terms...)
163 | self.register_builtin('trace', builtin_trace) # trace (+OnOff)
164 | self.register_builtin('true', builtin_true) # true
165 | self.register_builtin('ignore', builtin_ignore) # ignore (+P)
166 | self.register_builtin('var', builtin_var) # var (+Term)
167 | self.register_builtin('nonvar', builtin_nonvar) # nonvar (+Term)
168 |
169 | # these have become specials now
170 | # self.register_builtin('is', builtin_is) # is (?Ques, +Ans)
171 | # self.register_builtin('set', builtin_set) # set (?Var, +Val)
172 |
173 | # lists
174 |
175 | self.register_builtin('list_contains', builtin_list_contains)
176 | self.register_builtin('list_nth', builtin_list_nth)
177 | self.register_builtin('length', builtin_length) # length (+List, -Len)
178 | self.register_builtin('list_slice', builtin_list_slice) # list_slice (+Idx1, +Idx2, +List, -Slice)
179 | self.register_builtin('list_append', builtin_list_append) # list_append (?List, +Element)
180 | self.register_builtin('list_extend', builtin_list_extend) # list_extend (?List, +Element)
181 | self.register_builtin('list_str_join', builtin_list_str_join) # list_str_join (+Glue, +List, -Str)
182 | self.register_builtin('list_findall', builtin_list_findall) # list_findall (+Template, +Goal, -List)
183 |
184 | # dicts
185 |
186 | self.register_builtin('dict_put', builtin_dict_put) # dict_put (?Dict, +Key, +Value)
187 | self.register_builtin('dict_get', builtin_dict_get) # dict_get (+Dict, ?Key, -Value)
188 |
189 | # sets
190 |
191 | self.register_builtin('set_add', builtin_set_add) # set_add (?Set, +Value)
192 | self.register_builtin('set_get', builtin_set_get) # set_get (+Set, -Value)
193 | self.register_builtin('set_findall', builtin_set_findall) # set_findall (+Template, +Goal, -Set)
194 |
195 | # assert, rectract...
196 |
197 | self.register_builtin('assertz', builtin_assertz) # assertz (+P)
198 | self.register_builtin('retract', builtin_retract) # retract (+P)
199 | self.register_builtin('setz', builtin_setz) # setz (+P, +V)
200 | self.register_builtin('gensym', builtin_gensym) # gensym (+Root, -Unique)
201 |
202 | #
203 | # builtin functions
204 | #
205 |
206 | self.register_builtin_function ('format_str', builtin_format_str)
207 |
208 | # lists
209 |
210 | self.register_builtin_function ('list_max', builtin_list_max)
211 | self.register_builtin_function ('list_min', builtin_list_min)
212 | self.register_builtin_function ('list_sum', builtin_list_sum)
213 | self.register_builtin_function ('list_avg', builtin_list_avg)
214 | self.register_builtin_function ('list_len', builtin_list_len)
215 | self.register_builtin_function ('list_slice', builtin_list_slice_fn)
216 | self.register_builtin_function ('list_join', builtin_list_join_fn)
217 |
218 | def prolog_eval (self, term, env, location): # eval all variables within a term to constants
219 |
220 | #
221 | # implement Pseudo-Variables and -Predicates, e.g. USER:NAME
222 | #
223 |
224 | if (isinstance (term, Variable) or isinstance (term, Predicate)) and (":" in term.name):
225 |
226 | # import pdb; pdb.set_trace()
227 |
228 | parts = term.name.split(':')
229 |
230 | v = parts[0]
231 |
232 | if v[0].isupper():
233 | if not v in env:
234 | raise PrologRuntimeError('is: unbound variable %s.' % v, location)
235 | v = env[v]
236 |
237 | for part in parts[1:]:
238 |
239 | subparts = part.split('|')
240 |
241 | pattern = [v]
242 | wildcard_found = False
243 | for sp in subparts[1:]:
244 | if sp == '_':
245 | wildcard_found = True
246 | pattern.append('_1')
247 | else:
248 | pattern.append(sp)
249 |
250 | if not wildcard_found:
251 | pattern.append('_1')
252 |
253 | solutions = self.search_predicate (subparts[0], pattern, env=env)
254 | if len(solutions)<1:
255 | return Variable(term.name)
256 | v = solutions[0]['_1']
257 |
258 | return v
259 |
260 | #
261 | # regular eval semantics
262 | #
263 |
264 | if isinstance(term, Predicate):
265 |
266 | # unary builtin ?
267 |
268 | if len(term.args) == 1:
269 | op = unary_operators.get(term.name)
270 | if op:
271 |
272 | a = self.prolog_eval(term.args[0], env, location)
273 |
274 | if not isinstance (a, NumberLiteral):
275 | return None
276 |
277 | return op(a.f)
278 |
279 | # binary builtin ?
280 |
281 | op = binary_operators.get(term.name)
282 | if op:
283 | if len(term.args) != 2:
284 | return None
285 |
286 | a = self.prolog_eval(term.args[0], env, location)
287 |
288 | if not isinstance (a, NumberLiteral):
289 | return None
290 |
291 | b = self.prolog_eval(term.args[1], env, location)
292 |
293 | if not isinstance (b, NumberLiteral):
294 | return None
295 |
296 | return op(a.f, b.f)
297 |
298 | have_vars = False
299 | args = []
300 | for arg in term.args :
301 | a = self.prolog_eval(arg, env, location)
302 | if isinstance(a, Variable):
303 | have_vars = True
304 | args.append(a)
305 |
306 | # engine-provided builtin function ?
307 | if term.name in self.builtin_functions and not have_vars:
308 | return self.builtin_functions[term.name](args, env, self, location)
309 |
310 | return Predicate(term.name, args)
311 |
312 | if isinstance (term, ListLiteral):
313 | return ListLiteral (list(map (lambda x: self.prolog_eval(x, env, location), term.l)))
314 |
315 | if isinstance (term, Literal):
316 | return term
317 | if isinstance (term, MacroCall):
318 | return term
319 | if isinstance (term, Variable):
320 | ans = env.get(term.name)
321 | if not ans:
322 | return term
323 | else:
324 | return self.prolog_eval(ans, env, location)
325 |
326 | raise PrologError('Internal error: prolog_eval on unhandled object: %s (%s)' % (repr(term), term.__class__), location)
327 |
328 |
329 | # helper functions (used by builtin predicates)
330 | def prolog_get_int(self, term, env, location):
331 |
332 | t = self.prolog_eval (term, env, location)
333 |
334 | if not isinstance (t, NumberLiteral):
335 | raise PrologRuntimeError('Integer expected, %s found instead.' % term.__class__, location)
336 | return int(t.f)
337 |
338 | def prolog_get_float(self, term, env, location):
339 |
340 | t = self.prolog_eval (term, env, location)
341 |
342 | if not isinstance (t, NumberLiteral):
343 | raise PrologRuntimeError('Float expected, %s found instead.' % term.__class__, location)
344 | return t.f
345 |
346 | def prolog_get_string(self, term, env, location):
347 |
348 | t = self.prolog_eval (term, env, location)
349 |
350 | if not isinstance (t, StringLiteral):
351 | raise PrologRuntimeError('String expected, %s (%s) found instead.' % (unicode(t), t.__class__), location)
352 | return t.s
353 |
354 | def prolog_get_literal(self, term, env, location):
355 |
356 | t = self.prolog_eval (term, env, location)
357 |
358 | if not isinstance (t, Literal):
359 | raise PrologRuntimeError('Literal expected, %s %s found instead.' % (t.__class__, t), location)
360 | return t.get_literal()
361 |
362 | def prolog_get_bool(self, term, env, location):
363 |
364 | t = self.prolog_eval (term, env, location)
365 |
366 | if not isinstance(t, Predicate):
367 | raise PrologRuntimeError('Boolean expected, %s found instead.' % term.__class__, location)
368 | return t.name == 'true'
369 |
370 | def prolog_get_list(self, term, env, location):
371 |
372 | t = self.prolog_eval (term, env, location)
373 |
374 | if not isinstance(t, ListLiteral):
375 | raise PrologRuntimeError('List expected, %s (%s) found instead.' % (unicode(term), term.__class__), location)
376 | return t
377 |
378 | def prolog_get_dict(self, term, env, location):
379 |
380 | t = self.prolog_eval (term, env, location)
381 |
382 | if not isinstance(t, DictLiteral):
383 | raise PrologRuntimeError('Dict expected, %s found instead.' % term.__class__, location)
384 | return t
385 |
386 | def prolog_get_set(self, term, env, location):
387 |
388 | t = self.prolog_eval (term, env, location)
389 |
390 | if not isinstance(t, SetLiteral):
391 | raise PrologRuntimeError('Set expected, %s found instead.' % term.__class__, location)
392 | return t
393 |
394 | def prolog_get_variable(self, term, env, location):
395 |
396 | if not isinstance(term, Variable):
397 | raise PrologRuntimeError('Variable expected, %s found instead.' % term.__class__, location)
398 | return term.name
399 |
400 | def prolog_get_constant(self, term, env, location):
401 |
402 | t = self.prolog_eval (term, env, location)
403 |
404 | if not isinstance(t, Predicate):
405 | raise PrologRuntimeError('Constant expected, %s found instead.' % term.__class__, location)
406 | if len(t.args) >0:
407 | raise PrologRuntimeError('Constant expected, %s found instead.' % unicode(t), location)
408 | return t.name
409 |
410 | def prolog_get_predicate(self, term, env, location):
411 |
412 | t = self.prolog_eval (term, env, location)
413 |
414 | if t:
415 | if not isinstance(t, Predicate):
416 | raise PrologRuntimeError(u'Predicate expected, %s (%s) found instead.' % (unicode(t), t.__class__), location)
417 | return t
418 |
419 | if not isinstance(term, Predicate):
420 | raise PrologRuntimeError(u'Predicate expected, %s (%s) found instead.' % (unicode(term), term.__class__), location)
421 |
422 | return term
423 |
424 | def _unify (self, src, srcEnv, dest, destEnv, location, overwrite_vars) :
425 | "update dest env from src. return true if unification succeeds"
426 | # logging.debug("Unify %s %s to %s %s" % (src, srcEnv, dest, destEnv))
427 |
428 | # import pdb; pdb.set_trace()
429 | if isinstance (src, Variable):
430 | if (src.name == u'_'):
431 | return True
432 | # if ':' in src.name:
433 | # import pdb; pdb.set_trace()
434 | srcVal = self.prolog_eval(src, srcEnv, location)
435 | if isinstance (srcVal, Variable):
436 | return True
437 | else:
438 | return self._unify(srcVal, srcEnv, dest, destEnv, location, overwrite_vars)
439 |
440 | if isinstance (dest, Variable):
441 | if (dest.name == u'_'):
442 | return True
443 | destVal = self.prolog_eval(dest, destEnv, location) # evaluate destination
444 | if not isinstance(destVal, Variable) and not overwrite_vars:
445 | return self._unify(src, srcEnv, destVal, destEnv, location, overwrite_vars)
446 | else:
447 |
448 | # handle pseudo-vars?
449 |
450 | if ':' in dest.name:
451 | # import pdb; pdb.set_trace()
452 |
453 | pname, r_pattern, a_pattern = self._compute_retract_assert_patterns (dest, self.prolog_eval(src, srcEnv, location), destEnv, location)
454 |
455 | ovl = destEnv.get(ASSERT_OVERLAY_VAR_NAME)
456 | if ovl is None:
457 | ovl = LogicDBOverlay()
458 | else:
459 | ovl = ovl.clone()
460 |
461 | ovl.retract(Predicate ( pname, r_pattern))
462 | ovl.assertz(Clause ( Predicate(pname, a_pattern), location=location))
463 |
464 | destEnv[ASSERT_OVERLAY_VAR_NAME] = ovl
465 |
466 | else:
467 | destEnv[dest.name] = self.prolog_eval(src, srcEnv, location)
468 |
469 | return True # unifies. destination updated
470 |
471 | elif isinstance (src, Literal):
472 | srcVal = self.prolog_eval(src, srcEnv, location)
473 | destVal = self.prolog_eval(dest, destEnv, location)
474 | return srcVal == destVal
475 |
476 | elif isinstance (dest, Literal):
477 | return False
478 |
479 | else:
480 | if not isinstance(src, Predicate) or not isinstance(dest, Predicate):
481 | raise PrologRuntimeError (u'_unify: expected src/dest, got "%s" vs "%s"' % (repr(src), repr(dest)))
482 |
483 | if src.name != dest.name:
484 | return False
485 | elif len(src.args) != len(dest.args):
486 | return False
487 | else:
488 | for i in range(len(src.args)):
489 | if not self._unify(src.args[i], srcEnv, dest.args[i], destEnv, location, overwrite_vars):
490 | return False
491 |
492 | # always unify implicit overlay variable:
493 |
494 | if ASSERT_OVERLAY_VAR_NAME in srcEnv:
495 | destEnv[ASSERT_OVERLAY_VAR_NAME] = srcEnv[ASSERT_OVERLAY_VAR_NAME]
496 |
497 | return True
498 |
499 | def _trace (self, label, goal):
500 |
501 | if not self.trace:
502 | return
503 |
504 | # logging.debug ('label: %s, goal: %s' % (label, unicode(goal)))
505 |
506 | depth = goal.get_depth()
507 | # ind = depth * ' ' + len(label) * ' '
508 |
509 | res = u'!' if goal.negate else u''
510 |
511 | if goal.head:
512 | res += limit_str(unicode(goal.head), 60)
513 | else:
514 | res += u'TOP'
515 | res += ' '
516 |
517 | for i, t in enumerate(goal.terms):
518 | if i == goal.inx:
519 | res += u" -> " + limit_str(unicode(t), 60)
520 |
521 | res += ' [' + unicode(goal.location) + ']'
522 |
523 | # indent = depth*' ' + len(label) * ' '
524 | indent = depth*' '
525 |
526 | logging.info(u"%s %s: %s" % (indent, label, res))
527 |
528 | for k in sorted(goal.env):
529 | if k != ASSERT_OVERLAY_VAR_NAME:
530 | logging.info(u"%s %s=%s" % (indent, k, limit_str(repr(goal.env[k]), 100)))
531 |
532 | if ASSERT_OVERLAY_VAR_NAME in goal.env:
533 | goal.env[ASSERT_OVERLAY_VAR_NAME].log_trace(indent)
534 |
535 | # res += u'env=%s' % unicode(self.env)
536 |
537 | def _trace_fn (self, label, env):
538 |
539 | if not self.trace:
540 | return
541 |
542 | indent = ' '
543 |
544 | logging.info(u"%s %s" % (indent, label))
545 |
546 | # import pdb; pdb.set_trace()
547 |
548 | for k in sorted(env):
549 | logging.info(u"%s %s=%s" % (indent, k, limit_str(repr(env[k]), 80)))
550 |
551 | def _finish_goal (self, g, succeed, stack, solutions):
552 |
553 | while True:
554 |
555 | succ = not succeed if g.negate else succeed
556 |
557 | if succ:
558 | self._trace ('SUCCESS ', g)
559 |
560 | if g.parent == None : # Our original goal?
561 | solutions.append(g.env) # Record solution
562 |
563 | else:
564 | # stack up shallow copy of parent goal to resume
565 | parent = PrologGoal (head = g.parent.head,
566 | terms = g.parent.terms,
567 | parent = g.parent.parent,
568 | env = copy.copy(g.parent.env),
569 | negate = g.parent.negate,
570 | inx = g.parent.inx,
571 | location = g.parent.location)
572 | self._unify (g.head, g.env,
573 | parent.terms[parent.inx], parent.env, g.location, overwrite_vars = True)
574 | parent.inx = parent.inx+1 # advance to next goal in body
575 | stack.append(parent) # put it on the stack
576 |
577 | break
578 |
579 | else:
580 | self._trace ('FAIL ', g)
581 |
582 | if g.parent == None : # Our original goal?
583 | break
584 |
585 | else:
586 | # prepare shallow copy of parent goal to resume
587 | parent = PrologGoal (head = g.parent.head,
588 | terms = g.parent.terms,
589 | parent = g.parent.parent,
590 | env = copy.copy(g.parent.env),
591 | negate = g.parent.negate,
592 | inx = g.parent.inx,
593 | location = g.parent.location)
594 | self._unify (g.head, g.env,
595 | parent.terms[parent.inx], parent.env, g.location, overwrite_vars = True)
596 | g = parent
597 | succeed = False
598 |
599 | def apply_overlay (self, module, solution, commit=True):
600 |
601 | if not ASSERT_OVERLAY_VAR_NAME in solution:
602 | return
603 |
604 | solution[ASSERT_OVERLAY_VAR_NAME].do_apply(module, self.db, commit=True)
605 |
606 | def _compute_retract_assert_patterns (self, arg_Var, arg_Val, env, location):
607 |
608 | parts = arg_Var.name.split(':')
609 |
610 | v = parts[0]
611 |
612 | if v[0].isupper():
613 | if not v in env:
614 | raise PrologRuntimeError('%s: unbound variable %s.' % (arg_Var.name, v), location)
615 | v = env[v]
616 | else:
617 | v = Predicate(v)
618 |
619 | for part in parts[1:len(parts)-1]:
620 |
621 | subparts = part.split('|')
622 |
623 | pattern = [v]
624 | wildcard_found = False
625 | for sp in subparts[1:]:
626 | if sp == '_':
627 | wildcard_found = True
628 | pattern.append('_1')
629 | else:
630 | pattern.append(Predicate(sp))
631 |
632 | if not wildcard_found:
633 | pattern.append('_1')
634 |
635 | solutions = self.search_predicate (subparts[0], pattern, env=env)
636 | if len(solutions)<1:
637 | raise PrologRuntimeError(u'is: failed to match part "%s" of "%s".' % (part, unicode(arg_Var)), location)
638 | v = solutions[0]['_1']
639 |
640 | lastpart = parts[len(parts)-1]
641 |
642 | subparts = lastpart.split('|')
643 |
644 | r_pattern = [v]
645 | a_pattern = [v]
646 | wildcard_found = False
647 | for sp in subparts[1:]:
648 | if sp == '_':
649 | wildcard_found = True
650 | r_pattern.append(Variable('_'))
651 | a_pattern.append(arg_Val)
652 | else:
653 | r_pattern.append(Predicate(sp))
654 | a_pattern.append(Predicate(sp))
655 |
656 | if not wildcard_found:
657 | r_pattern.append(Variable('_'))
658 | a_pattern.append(arg_Val)
659 |
660 | return subparts[0], r_pattern, a_pattern
661 |
662 | def _special_is(self, g):
663 |
664 | self._trace ('CALLED SPECIAL is (?Var, +Val)', g)
665 |
666 | pred = g.terms[g.inx]
667 | args = pred.args
668 | if len(args) != 2:
669 | raise PrologRuntimeError('is: 2 args (?Var, +Val) expected.', g.location)
670 |
671 | arg_Var = self.prolog_eval(pred.args[0], g.env, g.location)
672 | arg_Val = self.prolog_eval(pred.args[1], g.env, g.location)
673 |
674 | # handle pseudo-variable assignment
675 | if (isinstance (arg_Var, Variable) or isinstance (arg_Var, Predicate)) and (":" in arg_Var.name):
676 |
677 | pname, r_pattern, a_pattern = self._compute_retract_assert_patterns (arg_Var, arg_Val, g.env, g.location)
678 |
679 | g.env = do_retract ({}, Predicate ( pname, r_pattern), res=g.env)
680 | g.env = do_assertz ({}, Clause ( Predicate(pname, a_pattern), location=g.location), res=g.env)
681 |
682 | return True
683 |
684 | # regular is/2 semantics
685 |
686 | if isinstance(arg_Var, Variable):
687 | if arg_Var.name != u'_':
688 | g.env[arg_Var.name] = arg_Val # Set variable
689 | return True
690 |
691 | if arg_Var != arg_Val:
692 | return False
693 |
694 | return True
695 |
696 | def _special_set(self, g):
697 |
698 | self._trace ('CALLED BUILTIN set (-Var, +Val)', g)
699 |
700 | pred = g.terms[g.inx]
701 | args = pred.args
702 | if len(args) != 2:
703 | raise PrologRuntimeError('set: 2 args (-Var, +Val) expected.', g.location)
704 |
705 | arg_Var = pred.args[0]
706 | arg_Val = self.prolog_eval(pred.args[1], g.env, g.location)
707 |
708 | # handle pseudo-variable assignment
709 | if (isinstance (arg_Var, Variable) or isinstance (arg_Var, Predicate)) and (":" in arg_Var.name):
710 |
711 | pname, r_pattern, a_pattern = self._compute_retract_assert_patterns (arg_Var, arg_Val, g.env, g.location)
712 |
713 | # import pdb; pdb.set_trace()
714 |
715 | g.env = do_retract ({}, Predicate ( pname, r_pattern), res=g.env)
716 | g.env = do_assertz ({}, Clause ( Predicate(pname, a_pattern), location=g.location), res=g.env)
717 |
718 | return True
719 |
720 | # regular set/2 semantics
721 |
722 | if not isinstance(arg_Var, Variable):
723 | raise PrologRuntimeError('set: arg 0 Variable expected, %s (%s) found instead.' % (unicode(arg_Var), arg_Var.__class__), g.location)
724 |
725 | g.env[arg_Var.name] = arg_Val # Set variable
726 |
727 | return True
728 |
729 | def search (self, a_clause, env={}):
730 |
731 | if a_clause.body is None:
732 | return [{}]
733 |
734 | if isinstance (a_clause.body, Predicate):
735 | if a_clause.body.name == 'and':
736 | terms = a_clause.body.args
737 | else:
738 | terms = [ a_clause.body ]
739 | else:
740 | raise PrologRuntimeError (u'search: expected predicate in body, got "%s" !' % unicode(a_clause))
741 |
742 | stack = [ PrologGoal (a_clause.head, terms, env=copy.copy(env), location=a_clause.location) ]
743 | solutions = []
744 |
745 | ts_start = time.time()
746 |
747 | while stack :
748 | g = stack.pop() # Next goal to consider
749 |
750 | self._trace ('CONSIDER', g)
751 |
752 | if g.inx >= len(g.terms) : # Is this one finished?
753 | self._finish_goal (g, True, stack, solutions)
754 | continue
755 |
756 | # No. more to do with this goal.
757 | pred = g.terms[g.inx] # what we want to solve
758 |
759 | if not isinstance(pred, Predicate):
760 | raise PrologRuntimeError (u'search: encountered "%s" (%s) when I expected a predicate!' % (unicode(pred), pred.__class__), g.location )
761 | name = pred.name
762 |
763 | # FIXME: debug only
764 | # if name == 'ias':
765 | # import pdb; pdb.set_trace()
766 |
767 | if name in builtin_specials:
768 |
769 | if name == 'cut': # zap the competition for the current goal
770 |
771 | # logging.debug ("CUT: stack before %s" % repr(stack))
772 | # import pdb; pdb.set_trace()
773 |
774 | while len(stack)>0 and stack[len(stack)-1].head and stack[len(stack)-1].head.name == g.parent.head.name:
775 | stack.pop()
776 |
777 | # logging.debug ("CUT: stack after %s" % repr(stack))
778 |
779 | elif name == 'fail': # Dont succeed
780 | self._finish_goal (g, False, stack, solutions)
781 | continue
782 |
783 | elif name == 'not':
784 | # insert negated sub-guoal
785 | stack.append(PrologGoal(pred, pred.args, g, env=copy.copy(g.env), negate=True, location=g.location))
786 | continue
787 |
788 | elif name == 'or':
789 |
790 | # logging.debug (' or clause detected.')
791 |
792 | # import pdb; pdb.set_trace()
793 | for subgoal in reversed(pred.args):
794 | or_subg = PrologGoal(pred, [subgoal], g, env=copy.copy(g.env), location=g.location)
795 | self._trace (' OR', or_subg)
796 | # logging.debug (' subgoal: %s' % subgoal)
797 | stack.append(or_subg)
798 |
799 | continue
800 |
801 | elif name == 'and':
802 | stack.append(PrologGoal(pred, pred.args, g, env=copy.copy(g.env), location=g.location))
803 | continue
804 |
805 | elif name == 'is':
806 | if not (self._special_is (g)):
807 | self._finish_goal (g, False, stack, solutions)
808 | continue
809 |
810 | elif name == 'set':
811 | if not (self._special_set (g)):
812 | self._finish_goal (g, False, stack, solutions)
813 | continue
814 |
815 | g.inx = g.inx + 1 # Succeed. resume self.
816 | stack.append(g)
817 | continue
818 |
819 | # builtin predicate ?
820 |
821 | if pred.name in self.builtins:
822 | bindings = self.builtins[pred.name](g, self)
823 | if bindings:
824 |
825 | self._trace ('SUCCESS FROM BUILTIN ', g)
826 |
827 | g.inx = g.inx + 1
828 | if type(bindings) is list:
829 |
830 | for b in reversed(bindings):
831 | new_env = copy.copy(g.env)
832 | new_env.update(b)
833 | stack.append(PrologGoal(g.head, g.terms, parent=g.parent, env=new_env, inx=g.inx, location=g.location))
834 |
835 | else:
836 | stack.append(g)
837 |
838 | else:
839 | self._finish_goal (g, False, stack, solutions)
840 |
841 | continue
842 |
843 | # Not special. look up in rule database
844 |
845 | static_filter = {}
846 | for i, a in enumerate(pred.args):
847 | ca = self.prolog_eval(a, g.env, g.location)
848 | if isinstance(ca, Predicate) and len(ca.args)==0:
849 | static_filter[i] = ca.name
850 | # if len(static_filter)>0:
851 | # if pred.name == 'not_dog':
852 | # import pdb; pdb.set_trace()
853 | clauses = self.db.lookup(pred.name, len(pred.args), overlay=g.env.get(ASSERT_OVERLAY_VAR_NAME), sf=static_filter)
854 |
855 | # if len(clauses) == 0:
856 | # # fail
857 | # self._finish_goal (g, False, stack, solutions)
858 | # continue
859 |
860 | success = False
861 |
862 | for clause in reversed(clauses):
863 |
864 | if len(clause.head.args) != len(pred.args):
865 | continue
866 |
867 | # logging.debug('clause: %s' % clause)
868 |
869 | # stack up child subgoal
870 |
871 | if clause.body:
872 | child = PrologGoal(clause.head, [clause.body], g, env={}, location=clause.location)
873 | else:
874 | child = PrologGoal(clause.head, [], g, env={}, location=clause.location)
875 |
876 | ans = self._unify (pred, g.env, clause.head, child.env, g.location, overwrite_vars = False)
877 | if ans: # if unifies, stack it up
878 | stack.append(child)
879 | success = True
880 | # logging.debug ("Queue %s" % str(child))
881 |
882 | if not success:
883 | # make sure we explicitly fail for proper negation support
884 | self._finish_goal (g, False, stack, solutions)
885 |
886 | # profiling
887 | ts_delay = time.time() - ts_start
888 | # logging.debug (u'runtime: search for %s took %fs.' % (unicode(a_clause), ts_delay))
889 | if ts_delay>SLOW_QUERY_TS:
890 | logging.warn (u'runtime: SLOW search for %s took %fs.' % (unicode(a_clause), ts_delay))
891 | # import pdb; pdb.set_trace()
892 |
893 | return solutions
894 |
895 | def search_predicate(self, name, args, env={}, location=None):
896 |
897 | """ convenience function: build Clause/Predicate structure, translate python strings in args
898 | into Predicates/Variables by Prolog conventions (lowercase: predicate, uppercase: variable) """
899 |
900 | if not location:
901 | location = SourceLocation('', 0, 0)
902 |
903 | solutions = self.search(Clause(body=build_predicate(name, args), location=location), env=env)
904 |
905 | return solutions
906 |
907 |
--------------------------------------------------------------------------------