├── .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 | --------------------------------------------------------------------------------