├── .gitignore ├── LICENSE ├── README.md ├── bench_fib.py └── logicpy ├── __init__.py ├── __main__.py ├── builtin.py ├── core.py ├── data.py ├── debug.py ├── predicate.py ├── result.py ├── structure.py ├── tests.py └── util └── getch.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/kate,python,kdevelop4,jupyternotebook 3 | 4 | ### JupyterNotebook ### 5 | .ipynb_checkpoints 6 | */.ipynb_checkpoints/* 7 | 8 | # Remove previous ipynb_checkpoints 9 | # git rm -r .ipynb_checkpoints/ 10 | # 11 | ### Kate ### 12 | # Swap Files # 13 | .*.kate-swp 14 | .swp.* 15 | 16 | ### KDevelop4 ### 17 | *.kdev4 18 | .kdev4/ 19 | 20 | ### Python ### 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | .hypothesis/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | 122 | # End of https://www.gitignore.io/api/kate,python,kdevelop4,jupyternotebook 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Evert Heylen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Prolog implementation in Python 3 | 4 | *Wanted: Better name* 5 | 6 | While following the course ["Declarative Languages" at KULeuven](https://onderwijsaanbod.kuleuven.be/syllabi/e/H0N03AE.htm#activetab=doelstellingen_idm5137664) I got inspired to make a Prolog implementation in Python, while relying on Python's parser. This is the result: 7 | 8 | ```python 9 | from logicpy import * 10 | u, n = Universe().and_namespace() 11 | 12 | n.parent[_.alice, _.bob] = True 13 | n.parent[_.alice, _.charlie] = True 14 | n.sibling[_.A, _.B] = n.parent(_.X, _.A) & n.parent(_.X, _.B) & (_.A != _.B) 15 | 16 | u.simple_query(n.sibling(_.X, _.Y)) 17 | # returns [{'X': Atom('bob'), 'Y': Atom('charlie')}, 18 | # {'X': Atom('charlie'), 'Y': Atom('bob')}] 19 | ``` 20 | 21 | There is also a kind of shell, which has some tricks so you can write your 22 | queries more naturally. Use it through `u.interactive()`: 23 | 24 | ``` 25 | ? parent(alice, Child) 26 | {Child = bob}; 27 | {Child = charlie}; 28 | ? 29 | ``` 30 | 31 | More examples can be found in the tests (`logicpy/tests.py`). There you will find more features: 32 | 33 | - **Evaluation of expressions (mainly math)**: I pulled a C++ for this and used the bitshift operators: `_.X << _.A * 2` will unify `X` with double of `A`, as long as `A` is instantiated. 34 | - **Cuts**: Just use `cut` 35 | - **Comparisons**: As you would expect. 36 | 37 | 38 | ## Why use it? 39 | 40 | It has, obviously, perfect integration with Python. There are three main ways to integrate your functionality, which are best described using their return value. 41 | 42 | ### My function doesn't return anything useful, it only performs some work. 43 | 44 | Use the `@runnable` decorator. Arguments given to your function will be evaluated. Used as a predicate, it will *always* succeed. Example: 45 | 46 | @runnable 47 | def add_article(title, text): 48 | requests.post(".../article/add", {'title': title, 'text': text}) 49 | 50 | # Can be used as: 51 | n.generate_intro[_.Name] = add_article("Intro " + _.Name, "Hello, I am " + _.Name + ", happy to be here.") 52 | 53 | 54 | ### My function returns a Boolean 55 | 56 | Use the `@provable` decorator. This works almost exactly like `@runnable`, but depending on the truthiness of the return value it will succeed or fail. 57 | 58 | 59 | ### My function returns some valuable result 60 | 61 | Use the `@evaluated` decorator. Again, the arguments itself are evaluated. Example: 62 | 63 | @evaluated 64 | def max_(x, y): 65 | return max(x, y) 66 | 67 | # Can be used as: 68 | Y << max_(X+5, 8) 69 | 70 | 71 | ### Want more control? (Debugging, multiple results, ...) 72 | 73 | Apart from those techniques, you can also subclass from `MonoArg` or `MultiArg` (or `Structure` if you really want), and implement the `prove(result, debugger)` method. Yield all results that you find ok. This gives you the most control, but requires more knowledge about the inner workings of this library. 74 | 75 | 76 | ## Why not use it? 77 | 78 | I didn't add any metaprogramming, since this would logically be Python's job. There is no 'standard library'. There are still going to be a lot of bugs. This project contains more lines of Python than the total amount of lines of Prolog I have written in my life, so some things might behave unexpectedly (but I wouldn't know currently). It's more of a proof-of-concept. 79 | 80 | However, the biggest disadvantage might be performance. It's pretty slow. 81 | 82 | 83 | ## How does it work? 84 | 85 | - Lots of generators for a Prolog-like runtime (works pretty well, see the backtracking implementation in `logicpy/builtin.py`!) 86 | - Some little hacky tricks to provide the interface (these are somewhat more brittle) 87 | 88 | 89 | ## License? 90 | 91 | This project is licensed under the MIT License. 92 | -------------------------------------------------------------------------------- /bench_fib.py: -------------------------------------------------------------------------------- 1 | 2 | from logicpy import * 3 | from logicpy.core import * 4 | from logicpy.builtin import * 5 | from logicpy.predicate import * 6 | from logicpy.data import * 7 | 8 | import time 9 | 10 | u, n = Universe().and_namespace() 11 | 12 | n.fib[0, 1] = True 13 | n.fib[1, 2] = True 14 | n.fib[_.N, _.Res] = and_( 15 | _.N > 1, 16 | _.N1 << _.N - 1, 17 | _.N2 << _.N - 2, 18 | n.fib(_.N1, _.Res1), 19 | n.fib(_.N2, _.Res2), 20 | _.Res << _.Res1 + _.Res2) 21 | 22 | fib = lambda x: {0: 1, 1: 2, 2: 3}.get(x) or fib(x-1) + fib(x-2) 23 | 24 | try: 25 | for arg in range(10): 26 | check = fib(arg) 27 | 28 | try: 29 | start = time.time() 30 | res = u.simple_query(n.fib(arg, _.X))[0]['X'] 31 | end = time.time() 32 | 33 | assert check == res, f"{check} != {res}" 34 | print(f"{arg}\t{end - start}") 35 | except Exception as e: 36 | print("\nFAIL --------------------------------------------------") 37 | print(e, "\n") 38 | print(u.simple_query(n.fib(arg, _.X), debug=True)) 39 | break 40 | except KeyboardInterrupt: 41 | pass 42 | 43 | -------------------------------------------------------------------------------- /logicpy/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Prolog: 3 | # parent(alice, bob). 4 | # parent(alice, charlie). 5 | # sibling(X, Y) :- parent(P, X), parent(P, Y). 6 | 7 | # LogiPy: 8 | # from logipy import Universe, _ 9 | # # terms: _.foo, Term('FOOOOBAR') 10 | # # --> upcasted to Atom/Compound 11 | # # vars: _.Bar, _._bar, Variable('quuz') 12 | # # unbound var: _ 13 | # 14 | # 15 | # universe = Universe() 16 | # u = universe.definer() 17 | # u.parent[_.alice, _.bob] = True 18 | # u.parent[_.alice, _.charlie] = True 19 | # u.sibling[X, Y] = u.parent(P, X) & u.parent(_.P, _.Y) 20 | 21 | from .core import Universe, Underscore 22 | from .builtin import * 23 | 24 | _ = Underscore() 25 | 26 | __all__ = ('_', 'Universe', 'evaluated', 'runnable', 'provable') + shell_builtins 27 | -------------------------------------------------------------------------------- /logicpy/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | from logicpy import * 3 | Universe().interactive() 4 | -------------------------------------------------------------------------------- /logicpy/builtin.py: -------------------------------------------------------------------------------- 1 | 2 | import operator 3 | from functools import wraps 4 | 5 | from logicpy.structure import Structure, MultiArg, BinaryArg, MonoArg 6 | from logicpy.data import Compound, EvalCompound, Variable, Term, instantiate 7 | from logicpy.result import ResultException, UnificationFail 8 | 9 | shell_builtins = ('True_', 'Fail', 'and_', 'or_', 'max_', 'min_', 'abs_', 'cut', 'neg', 'write') 10 | 11 | 12 | class TrueCls(Structure): 13 | def prove(self, result, dbg): 14 | dbg.proven(self, result) 15 | yield result 16 | 17 | __repr__ = __str__ = lambda s: "True" 18 | 19 | True_ = TrueCls() 20 | 21 | 22 | class FailCls(Structure): 23 | def prove(self, result, dbg): 24 | dbg.prove(self, result) 25 | return 26 | yield 27 | 28 | __repr__ = __str__ = lambda s: "Fail" 29 | 30 | Fail = FailCls() 31 | 32 | 33 | class and_(MultiArg): 34 | op = '&' 35 | 36 | def __and__(self, other): 37 | return and_(*(self.args + (other,))) 38 | 39 | def prove(self, result, dbg): 40 | # Backtracking implementation! 41 | dbg.prove(self, result) 42 | return self.prove_arg(0, result, dbg) 43 | 44 | def prove_arg(self, n, result, dbg): 45 | if n >= len(self.args): 46 | dbg.proven(self, result) 47 | yield result 48 | return 49 | 50 | arg = self.args[n] 51 | for new_result in arg.prove(result, dbg.from_next()): 52 | yield from self.prove_arg(n+1, new_result, dbg) 53 | 54 | 55 | class or_(MultiArg): 56 | op = '|' 57 | 58 | def __or__(self, other): 59 | return or_(*(self.args + (other,))) 60 | 61 | def prove(self, result, dbg): 62 | dbg.prove(self, result) 63 | for arg in self.args: 64 | yield from arg.prove(result, dbg.next()) 65 | 66 | 67 | class unify(BinaryArg): 68 | op = '==' 69 | 70 | @property 71 | def args(self): 72 | return (self.left, self.right) 73 | 74 | def prove(self, result, dbg): 75 | result = result | {(self.left, self.right)} 76 | try: 77 | mgu = result.mgu() 78 | dbg.proven(self, mgu) 79 | yield mgu 80 | except UnificationFail as e: 81 | dbg.output(f"Unification failed: {e}") 82 | 83 | def __bool__(self): 84 | if isinstance(self.left, Term): 85 | return self.left.really_equal(self.right) 86 | else: 87 | return self.right.really_equal(self.left) 88 | 89 | 90 | class PredicateCut(Exception): 91 | pass 92 | 93 | 94 | class _Cut(Structure): 95 | def prove(self, result, dbg): 96 | dbg.output("Cut!") 97 | yield result 98 | raise PredicateCut() 99 | 100 | cut = _Cut() 101 | 102 | 103 | class neg(MonoArg): 104 | def prove(self, result, dbg): 105 | for _ in self.arg.prove(result, dbg.next()): 106 | return 107 | yield result 108 | 109 | 110 | # Builtin operator support (mainly math) 111 | # -------------------------------------- 112 | 113 | class EvalException(Exception): 114 | pass 115 | 116 | 117 | def evaluate(expr): 118 | if isinstance(expr, EvalCompound): 119 | try: 120 | return expr.func(*(evaluate(c) for c in expr.children)) 121 | except EvalException as e: 122 | raise e # rethrow 123 | except Exception as e: 124 | raise EvalException("Couldn't do operation: " + str(e)) 125 | else: 126 | return expr 127 | 128 | 129 | class Evaluation(BinaryArg): 130 | op = '<<' 131 | 132 | def prove(self, result, dbg): 133 | dbg.prove(self, result) 134 | try: 135 | res = evaluate(instantiate(self.right, result)) 136 | result = result | {(self.left, res)} 137 | mgu = result.mgu() 138 | dbg.proven(self, mgu) 139 | yield mgu 140 | except (EvalException, ResultException) as e: 141 | dbg.output(f"Eval failed: {e}") 142 | 143 | 144 | class Comparison(BinaryArg): 145 | def prove(self, result, dbg): 146 | dbg.prove(self, result) 147 | try: 148 | l = evaluate(instantiate(self.left, result)) 149 | r = evaluate(instantiate(self.right, result)) 150 | if self.compare(l, r): 151 | dbg.proven(self, result) 152 | yield result 153 | except (EvalException, Uninstantiated) as e: 154 | dbg.output(f"Comparison failed: {e}") 155 | 156 | 157 | 158 | class Lower(Comparison): 159 | op = '<' 160 | compare = lambda s, l, r: l < r 161 | 162 | 163 | class LowerOrEqual(Comparison): 164 | op = '<=' 165 | compare = lambda s, l, r: l <= r 166 | 167 | 168 | class Greater(Comparison): 169 | op = '>' 170 | compare = lambda s, l, r: l > r 171 | 172 | 173 | class GreaterOrEqual(Comparison): 174 | op = '>=' 175 | compare = lambda s, l, r: l >= r 176 | 177 | 178 | def evaluated(func): 179 | "Turns a function into a term that is evaluated at runtime" 180 | @wraps(func) 181 | def wrapper(*args): 182 | return EvalCompound(func.__name__, func, args) 183 | return wrapper 184 | 185 | 186 | @evaluated 187 | def max_(x, y): 188 | return max(x, y) 189 | 190 | 191 | @evaluated 192 | def min_(x, y): 193 | return min(x, y) 194 | 195 | 196 | @evaluated 197 | def abs_(x): 198 | return abs(x) 199 | 200 | 201 | def runnable(func, skip_result_check=True): 202 | """Turns a function into a predicate that is ran with evaluated arguments. 203 | Does not check the result, always succeeds. 204 | """ 205 | 206 | class Runnable(MultiArg): 207 | def prove(self, result, dbg): 208 | dbg.prove(self, result) 209 | args = "" 210 | try: 211 | args = tuple(evaluate(instantiate(a, result)) for a in self.args) 212 | func_res = func(*args) 213 | if skip_result_check or func_res: 214 | yield result 215 | except Exception as e: 216 | dbg.output(f"Calling {func.__name__} with args {args} failed: {e}") 217 | 218 | return Runnable 219 | 220 | 221 | def provable(func): 222 | """Turns a function into a predicate that is ran with evaluated arguments, 223 | and will fail or succeed based on the thruthiness of the return value. 224 | """ 225 | return runnable(func, skip_result_check=False) 226 | 227 | 228 | write = runnable(print) 229 | -------------------------------------------------------------------------------- /logicpy/core.py: -------------------------------------------------------------------------------- 1 | 2 | from logicpy.predicate import Predicate, NoArgument 3 | from logicpy.data import Variable, Atom, NamedTerm 4 | from logicpy.builtin import Fail, unify, shell_builtins 5 | from logicpy.result import Result 6 | from logicpy.structure import Structure 7 | from logicpy.debug import Debugger, NoDebugger 8 | from logicpy.util.getch import getch 9 | 10 | 11 | class Universe: 12 | def __init__(self): 13 | self._predicates = {} 14 | 15 | def namespace(self): 16 | return Namespace(self) 17 | 18 | def and_namespace(self): 19 | return self, self.namespace() 20 | 21 | def define(self, clause): 22 | sig = clause.signature 23 | pred = self._predicates.setdefault(sig, Predicate(sig)) 24 | pred.add_clause(clause) 25 | 26 | def get_pred(self, sig): 27 | if sig in self._predicates: 28 | return self._predicates[sig] 29 | else: 30 | return None 31 | 32 | def query(self, struc, *, debug=False): 33 | struc = struc.with_scope(0) 34 | yield from struc.prove(Result(), Debugger() if debug else NoDebugger()) 35 | 36 | def simple_query(self, struc, limit=None, **kwargs): 37 | q = self.query(struc, **kwargs) 38 | if limit is None: 39 | return [res.easy_dict() for res in q] 40 | else: 41 | return [next(q).easy_dict() for i in range(limit)] 42 | 43 | def ok(self, struc, **kwargs): 44 | for b in self.query(struc, **kwargs): 45 | return True 46 | return False 47 | 48 | def interactive(self): 49 | namespace = self.namespace() 50 | underscore = Underscore() 51 | 52 | import logicpy.builtin 53 | builtins = {k: getattr(logicpy.builtin, k) for k in shell_builtins} 54 | 55 | class InteractiveLocals: 56 | def __getitem__(glob, name): 57 | if name in builtins: 58 | return builtins[name] 59 | elif name in namespace: 60 | return getattr(namespace, name) 61 | else: 62 | return getattr(underscore, name) 63 | 64 | shell_locals = InteractiveLocals() 65 | 66 | while True: 67 | try: 68 | inp = input("? ") 69 | struc = eval(inp, {}, shell_locals) 70 | #print(f">>> got {struc!r}") 71 | if hasattr(struc, 'prove'): 72 | for res in self.query(struc): 73 | print(res, end='', flush=True) 74 | char = getch() 75 | print(char) 76 | if char in '\n.': 77 | break 78 | elif char == ';': 79 | pass 80 | else: 81 | print(f"Press ';' for more solutions, '.' or Enter to stop") 82 | else: 83 | print(f"Not a provable structure: {struc}") 84 | except EOFError: 85 | break 86 | except Exception as e: 87 | print(f"Error: {e}") 88 | 89 | def __str__(self): 90 | return f"Universe with {len(self._predicates)} predicates:\n "\ 91 | + "\n ".join(f"{k}: {v!r}" for k,v in self._predicates.items()) 92 | 93 | 94 | class Namespace: 95 | def __init__(self, univ): 96 | self.__dict__['_univ'] = univ 97 | self.__dict__['_names'] = {sig.name for sig, pred in univ._predicates.items()} 98 | 99 | def __contains__(self, name): 100 | return name in self._names 101 | 102 | def __getattr__(self, name): 103 | # getattr will cover the case of 'u.foo' 104 | # NoArgument covers the case of 'u.foo[bar]' and 'u.foo[bar] = quuz' 105 | return NoArgument(name, None, self._univ) 106 | 107 | def __setattr__(self, name, body): 108 | # setattr will cover the case of 'u.foo = bar' 109 | return NoArgument(name, body, self._univ) 110 | 111 | 112 | class Underscore(NamedTerm): 113 | def __init__(self, name='_', been_scoped=False): 114 | super().__init__(name, been_scoped) 115 | 116 | def with_scope(self, scope): 117 | # Just create a random variable 118 | return Variable("_", Structure.scope_id()) 119 | 120 | def __getattr__(self, name): 121 | if name[0].isupper() or name[0] == '_': # Variable 122 | return Variable(name) 123 | else: 124 | return Atom(name) # Atom will create Compounds when needed 125 | 126 | -------------------------------------------------------------------------------- /logicpy/data.py: -------------------------------------------------------------------------------- 1 | 2 | import operator 3 | 4 | 5 | # Free functions to enable working with 'foreign' constants 6 | # --------------------------------------------------------- 7 | 8 | def with_scope(obj, scope): 9 | if hasattr(obj, 'with_scope'): 10 | return obj.with_scope(scope) 11 | else: 12 | return obj 13 | 14 | 15 | def has_occurence(obj, var): 16 | if hasattr(obj, 'has_occurence'): 17 | return obj.has_occurence(var) 18 | else: 19 | return False 20 | 21 | 22 | def occurences(obj, O): 23 | if hasattr(obj, 'occurences'): 24 | return obj.occurences(O) 25 | 26 | 27 | def replace(obj, A, B): 28 | if obj is A or obj == A: 29 | return B 30 | elif hasattr(obj, 'replace'): 31 | return obj.replace(A, B) 32 | else: 33 | return obj 34 | 35 | 36 | def instantiate(expr, result): 37 | if hasattr(expr, 'instantiate'): 38 | return expr.instantiate(result) 39 | else: 40 | return expr 41 | 42 | 43 | 44 | # Terms and builtin operations 45 | # ---------------------------- 46 | 47 | 48 | def binary_compounder(name, func): 49 | def operation(self, other): 50 | return InfixEvalCompound(name, func, (self, other)) 51 | def rev_operation(self, other): 52 | return InfixEvalCompound(name, func, (other, self)) 53 | return operation, rev_operation 54 | 55 | 56 | def unary_compounder(name, func): 57 | def operation(self): 58 | return PrefixEvalCompound(name, func, (self,)) 59 | return operation 60 | 61 | 62 | class Term: 63 | def __init__(self, been_scoped=False): 64 | self.been_scoped = been_scoped 65 | 66 | def with_scope(self, scope): 67 | if self.been_scoped: 68 | return self 69 | else: 70 | return type(self)(been_scoped=True) 71 | 72 | # Basic operand support ............................... 73 | 74 | __add__, __radd__ = binary_compounder('+', operator.add) 75 | __sub__, __rsub__ = binary_compounder('-', operator.sub) 76 | __mul__, __rmul__ = binary_compounder('*', operator.mul) 77 | __div__, __rdiv__ = binary_compounder('/', operator.truediv) 78 | __floordiv__, __rfloordiv__ = binary_compounder('//', operator.floordiv) 79 | __mod__, __rmod__ = binary_compounder('%', operator.mod) 80 | __matmul__, __rmatmul__ = binary_compounder('@', operator.matmul) 81 | __pow__, __rpow__ = binary_compounder('**', operator.pow) 82 | 83 | __pos__ = unary_compounder('+', operator.pos) 84 | __neg__ = unary_compounder('-', operator.neg) 85 | 86 | 87 | # Comparisons ......................................... 88 | 89 | def __lt__(l, r): 90 | from logicpy.builtin import Lower 91 | return Lower(l, r) 92 | 93 | def __le__(l, r): 94 | from logicpy.builtin import LowerOrEqual 95 | return LowerOrEqual(l, r) 96 | 97 | def __gt__(l, r): 98 | from logicpy.builtin import Greater 99 | return Greater(l, r) 100 | 101 | def __ge__(l, r): 102 | from logicpy.builtin import GreaterOrEqual 103 | return GreaterOrEqual(l, r) 104 | 105 | 106 | # Some random operator overloads ...................... 107 | 108 | def __lshift__(self, other): 109 | "Replaces is in Prolog" 110 | from logicpy.builtin import Evaluation 111 | return Evaluation(self, other) 112 | 113 | def __rshift__(self, other): 114 | from logicpy.builtin import Evaluation 115 | return Evaluation(other, self) 116 | 117 | def __eq__(self, other): 118 | if self.been_scoped: 119 | return type(self) == type(other) and self.really_equal(other) 120 | else: 121 | from logicpy.builtin import unify 122 | return unify(self, other) 123 | 124 | def __ne__(self, other): 125 | if self.been_scoped: 126 | return type(self) != type(other) or (not self.really_equal(other)) 127 | else: 128 | from logicpy.builtin import unify, neg 129 | return neg(unify(self, other)) 130 | 131 | 132 | 133 | # Subclasses of Term: Atom, Compound, Variable and some shared functionality 134 | # -------------------------------------------------------------------------- 135 | 136 | class NamedTerm(Term): 137 | TERM_TYPE = 'Term' 138 | 139 | def __init__(self, name, been_scoped=False): 140 | super().__init__(been_scoped) 141 | self.name = name 142 | 143 | def __str__(self): 144 | return str(self.name) 145 | 146 | def __repr__(self): 147 | return f"{type(self).__name__}({self.name!r})" 148 | 149 | def really_equal(self, other): 150 | return type(self) == type(other) and self.name == other.name 151 | 152 | def __hash__(self): 153 | return hash(self.name) 154 | 155 | def has_occurence(self, var): 156 | return False # by default 157 | 158 | def occurences(self, O): 159 | pass 160 | 161 | def with_scope(self, scope): 162 | if self.been_scoped: 163 | return self 164 | else: 165 | return type(self)(self.name, been_scoped=True) 166 | 167 | 168 | class BasicTerm(NamedTerm): 169 | pass 170 | 171 | 172 | class Atom(BasicTerm): 173 | def __call__(self, *args): 174 | assert len(args) >= 1, "Creation of Compound needs at least 1 argument" 175 | return Compound(self.name, args) 176 | 177 | children = [] 178 | 179 | 180 | class NotInstantiated(Exception): 181 | pass 182 | 183 | 184 | class Compound(BasicTerm): 185 | def __init__(self, name, children, been_scoped=False): 186 | super().__init__(name, been_scoped) 187 | self.children = tuple(children) 188 | 189 | def __str__(self): 190 | return f"{self.name}({', '.join(map(str, self.children))})" 191 | 192 | def __repr__(self): 193 | return f"{type(self).__name__}({self.name!r}, {self.children!r})" 194 | 195 | def really_equal(self, other): 196 | return self.name == other.name and self.children == other.children 197 | 198 | def __hash__(self): 199 | return hash((self.name, self.children)) 200 | 201 | def has_occurence(self, var): 202 | return any(has_occurence(c, var) for c in self.children) 203 | 204 | def occurences(self, O): 205 | for c in self.children: 206 | occurences(c, O) 207 | 208 | def replace(self, A, B): 209 | new_children = tuple(replace(c, A, B) for c in self.children) 210 | return Compound(self.name, new_children, been_scoped=self.been_scoped) 211 | 212 | def with_scope(self, scope): 213 | return Compound(self.name, tuple(with_scope(c, scope) for c in self.children), been_scoped=True) 214 | 215 | def instantiate(self, result): 216 | return Compound(self.name, tuple(instantiate(c, result) for c in self.children), been_scoped=self.been_scoped) 217 | 218 | 219 | class EvalCompound(Compound): 220 | def __init__(self, name, func, children, been_scoped=False): 221 | super().__init__(name, children, been_scoped) 222 | self.func = func 223 | 224 | def replace(self, A, B): 225 | new_children = tuple(replace(c, A, B) for c in self.children) 226 | return EvalCompound(self.name, self.func, new_children, been_scoped=self.been_scoped) 227 | 228 | def with_scope(self, scope): 229 | return EvalCompound(self.name, self.func, tuple(with_scope(c, scope) for c in self.children), been_scoped=True) 230 | 231 | def instantiate(self, result): 232 | return EvalCompound(self.name, self.func, tuple(instantiate(c, result) for c in self.children), been_scoped=self.been_scoped) 233 | 234 | 235 | class InfixEvalCompound(EvalCompound): 236 | def __str__(self): 237 | return '(' + f" {self.name} ".join(map(str, self.children)) + ')' 238 | 239 | 240 | class PrefixEvalCompound(EvalCompound): 241 | def __str__(self): 242 | return self.name + " ".join(map(str, self.children)) 243 | 244 | 245 | class Variable(NamedTerm): 246 | def __init__(self, name, scope=None): 247 | super().__init__(name, scope is not None) 248 | self.name = name 249 | self.scope = scope 250 | 251 | def __str__(self): 252 | if self.scope: 253 | return f"{self.name}:{self.scope % 997}" 254 | else: 255 | return super().__str__() 256 | 257 | def __repr__(self): 258 | if self.scope: 259 | return f"Variable({self.name!r}, {self.scope})" 260 | else: 261 | return f"Variable({self.name!r})" 262 | 263 | def really_equal(self, other): 264 | return self.name == other.name and self.scope == other.scope 265 | 266 | def __hash__(self): 267 | return hash((self.name, self.scope)) 268 | 269 | def has_occurence(self, var): 270 | return self == var 271 | 272 | def occurences(self, O): 273 | O.add(self) 274 | 275 | def with_scope(self, scope): 276 | return Variable(self.name, scope) 277 | 278 | def instantiate(self, result): 279 | return result.get_var(self) 280 | 281 | -------------------------------------------------------------------------------- /logicpy/debug.py: -------------------------------------------------------------------------------- 1 | 2 | class NoDebugger: 3 | def prove(self, w, r): 4 | pass 5 | 6 | def output(self, text): 7 | pass 8 | 9 | def proven(self, w, r): 10 | pass 11 | 12 | def next(self): 13 | return self 14 | 15 | def from_next(self): 16 | return self 17 | 18 | def __bool__(self): 19 | return False 20 | 21 | 22 | def truncate(s, length): 23 | if len(s) <= length: 24 | return s 25 | else: 26 | return s[:length-3] + "..." 27 | 28 | 29 | class Debugger: 30 | def __init__(self, level=0, return_level=-1): 31 | self.level = level 32 | self.return_level = return_level 33 | 34 | def prove(self, what, res): 35 | padding = " " * self.level 36 | print(padding + f"-> {type(what).__name__} {what} with\t {res}") 37 | 38 | def proven(self, what, res): 39 | padding = " " * (self.return_level + 1) 40 | arrow = "---" * (self.level - self.return_level) 41 | arrow = "<" + arrow[1:-1] + " " 42 | print(padding + arrow + f"{type(what).__name__} {what} with\t {res}") 43 | 44 | def output(self, text): 45 | print(((self.level+1) * " ") + text) 46 | 47 | def next(self): 48 | return type(self)(self.level + 1, self.return_level) 49 | 50 | def from_next(self): 51 | return type(self)(self.level + 1, self.level) 52 | 53 | def __bool__(self): 54 | return True 55 | 56 | -------------------------------------------------------------------------------- /logicpy/predicate.py: -------------------------------------------------------------------------------- 1 | 2 | from collections import namedtuple 3 | 4 | from logicpy.structure import Structure, MultiArg 5 | from logicpy.builtin import True_, Fail, and_, or_, PredicateCut 6 | from logicpy.result import Result, UnificationFail 7 | from logicpy.data import with_scope, Variable 8 | 9 | 10 | class PredicateNotFound(Exception): 11 | pass 12 | 13 | 14 | 15 | class Signature(namedtuple('_Signature', ('name', 'arity'))): 16 | def __str__(self): 17 | return f"{self.name}/{self.arity}" 18 | 19 | __repr__ = __str__ 20 | 21 | 22 | class Clause: 23 | def __init__(self, name, args, body, univ): 24 | self.univ = univ 25 | self.signature = Signature(name, len(args)) 26 | self.args = args 27 | self.body = True_ if body is True else body 28 | if self.body and self.univ: 29 | self.univ.define(self) 30 | 31 | def __str__(self): 32 | return str(self.signature) 33 | 34 | def __repr__(self): 35 | return f"{self.signature.name}({', '.join(map(str, self.args))}) :- {self.body}" 36 | 37 | 38 | class NoArgument(Clause, Structure): 39 | """ This class can be used in many ways. It is the biggest price we pay for not having 40 | our own parser. Here are examples of all usages: 41 | 42 | 1) u.one = ... --> /0 clause 43 | 2) u.three[foo] = ... --> /1 or higher clause 44 | 3) u.foobar = u.five --> usage of /0 clause 45 | 4) u.foobar[A] = u.six(A) --> usage of /1 or higher clause 46 | 47 | """ 48 | 49 | def __init__(self, name, body, univ): 50 | # Protection against unwanted /0 base clauses 51 | self.am_i_new = Signature(name, 0) not in univ._predicates and body is not None 52 | Clause.__init__(self, name, (), body, univ) # Covers 1 and 2 (/0 clauses) 53 | 54 | def del_if_new(self): 55 | if self.am_i_new: 56 | # Remove the /0 variant since we're actually defining another arity 57 | del self.univ._predicates[self.signature] 58 | 59 | def __setitem__(self, args, body): 60 | # /1 or higher clauses 61 | self.del_if_new() 62 | if not isinstance(args, tuple): args = (args,) 63 | return Clause(self.signature.name, args, body, self.univ) 64 | 65 | def __call__(self, *args): 66 | # /1 or higher call 67 | self.del_if_new() 68 | return PredicateCall(self.univ, Signature(self.signature.name, len(args)), args) 69 | 70 | def prove(self, result, dbg): 71 | # Act like a PredicateCall (/0 structure) 72 | predcall = PredicateCall(self.univ, self.signature, ()) 73 | return predcall.prove(Result(), dbg.next()) 74 | 75 | 76 | class Predicate: 77 | def __init__(self, signature): 78 | self.signature = signature 79 | self.clauses = [] 80 | 81 | def add_clause(self, clause): 82 | self.clauses.append(clause) 83 | 84 | def __str__(self): 85 | return str(self.signature) 86 | 87 | def __repr__(self): 88 | return "; ".join(map(repr, clauses)) 89 | 90 | 91 | class PredicateCall(MultiArg): 92 | def __init__(self, univ, signature, args): 93 | self.univ = univ 94 | self.signature = signature 95 | super().__init__(*args) 96 | 97 | def __str__(self): 98 | return f"{self.signature.name}({', '.join(map(str, self.args))})" 99 | 100 | def __repr__(self): 101 | return f"{self.signature.name}({', '.join(map(repr, self.args))})" 102 | 103 | def with_scope(self, scope): 104 | return PredicateCall(self.univ, self.signature, [with_scope(a, scope) for a in self.args]) 105 | 106 | def prove(self, result, dbg): 107 | dbg.prove(self, result) 108 | pred = self.univ.get_pred(self.signature) 109 | 110 | if pred is None: 111 | raise PredicateNotFound(f"Couldn't find predicate with signature {self.signature}") 112 | else: 113 | try: 114 | for i, clause in enumerate(pred.clauses): 115 | scope = self.scope_id() 116 | structure = clause.body.with_scope(scope) 117 | 118 | arg_res = Result((with_scope(a, scope), b) for a, b in zip(clause.args, self.args)) 119 | try: 120 | total_res = (arg_res | result).mgu() 121 | dbg.output(f"Unified arguments for clause {i}") 122 | except UnificationFail as e: 123 | dbg.output(f"Failed to unify arguments for clause {i}: {e}") 124 | continue 125 | 126 | relevant_res = Result((a, b) for a, b in total_res if (a,b) in arg_res or (isinstance(a, Variable) and a.scope == scope)) 127 | 128 | clause_dbg = dbg.next() 129 | clause_dbg.prove(clause, relevant_res) 130 | 131 | for new_res in structure.prove(relevant_res, dbg or clause_dbg.from_next()): 132 | try: 133 | mgu = (new_res | result | arg_res).mgu() 134 | clause_dbg.proven(clause, mgu) 135 | yield mgu 136 | except UnificationFail as e: 137 | clause_dbg.output(f"Failed to unify resulting sets: {e}") 138 | except PredicateCut: 139 | pass # Look at how easy that is ;) 140 | -------------------------------------------------------------------------------- /logicpy/result.py: -------------------------------------------------------------------------------- 1 | 2 | from logicpy.data import Term, Variable, BasicTerm, has_occurence, replace 3 | from logicpy.debug import NoDebugger 4 | 5 | class ResultException(Exception): 6 | pass 7 | 8 | 9 | class UnificationFail(ResultException): 10 | pass 11 | 12 | 13 | class Uninstantiated(ResultException): 14 | pass 15 | 16 | 17 | class Result: 18 | def __init__(self, it = None, var_cache = None): 19 | self.var_cache = var_cache or {} 20 | if it: 21 | self.identities = frozenset(it) 22 | else: 23 | self.identities = frozenset() 24 | 25 | # Act like a proper set ............................... 26 | 27 | # Binary operations 28 | # no __r*__ versions, PyPy doesn't know those 29 | for fname in ['__and__', '__xor__', '__sub__', 30 | 'intersection', 'difference', 'symmetric_difference', 'union']: 31 | def passthrough(self, other, f = getattr(frozenset, fname)): 32 | return type(self)(f(self.identities, other.identities if isinstance(other, Result) else other)) 33 | locals()[fname] = passthrough 34 | 35 | def __or__(self, other): 36 | if isinstance(other, Result): 37 | # Often used, so let's write a faster version using var_cache 38 | overlap = self.var_cache.keys() & other.var_cache.keys() 39 | for var in overlap: 40 | if self.var_cache[var] != other.var_cache[var]: 41 | return FailResult() 42 | 43 | total_var_cache = {**self.var_cache, **other.var_cache} 44 | total_identities = self.identities | other.identities 45 | return type(self)(total_identities, total_var_cache) 46 | else: 47 | return type(self)(self.identities | other) 48 | 49 | def __len__(self): 50 | return len(self.identities) 51 | 52 | def __iter__(self): 53 | return iter(self.identities) 54 | 55 | def __contains__(self, obj): 56 | return obj in self.identities 57 | 58 | 59 | # Representation and easy usage ...................... 60 | 61 | def __str__(self): 62 | if len(self) == 0: 63 | return 'ok' 64 | return '{' + ', '.join(f"{L} = {R}" for L, R in self.identities) + '}' 65 | 66 | def easy_dict(self): 67 | return {L.name: R for L, R in self.identities if isinstance(L, Variable) and L.scope == 0} 68 | 69 | 70 | # Prolog additions .................................... 71 | 72 | def get_var(self, var): 73 | try: 74 | return self.var_cache[var] 75 | except KeyError: 76 | for A, B in self.identities: 77 | if A == var: 78 | self.var_cache[var] = B 79 | return B 80 | raise Uninstantiated(f"Uninstantiated: {var}") 81 | 82 | def mgu(self): 83 | return Result(Result.martelli_montanari(set(self.identities))) 84 | 85 | @staticmethod 86 | def martelli_montanari(E): 87 | from logicpy.structure import Structure 88 | 89 | if len(E) == 0: 90 | return E 91 | 92 | did_a_thing = True 93 | tried = set() 94 | 95 | while True: 96 | untried = E - tried 97 | if len(untried) == 0: break 98 | (A, B) = untried.pop() 99 | E.remove((A, B)) 100 | did_a_thing = True # Assume and unset later 101 | 102 | if not isinstance(A, Variable) and isinstance(B, Variable): 103 | # switch 104 | E.add((B, A)) 105 | elif isinstance(A, BasicTerm) and isinstance(B, BasicTerm): 106 | # peel 107 | if A.name == B.name and len(A.children) == len(B.children): 108 | E.update(zip(A.children, B.children)) 109 | else: 110 | raise UnificationFail(f"Conflict {A}, {B}") 111 | elif isinstance(A, Variable) and (not isinstance(B, Variable) or not A.really_equal(B)): 112 | # substitute 113 | if has_occurence(B, A): 114 | raise UnificationFail(f"Occurs check {A}, {B}") 115 | # While not very elegant, this is substantially faster in PyPy 116 | # In CPython, it's about the same 117 | remove_from_E = set() 118 | add_to_E = list() 119 | did_a_thing = False 120 | for t in E: 121 | nt = (replace(t[0], A, B), replace(t[1], A, B)) 122 | if t != nt: 123 | did_a_thing = True 124 | remove_from_E.add(t) 125 | add_to_E.append(nt) 126 | 127 | if did_a_thing: 128 | E -= remove_from_E 129 | E.update(add_to_E) 130 | 131 | E.add((A, B)) # Add it back 132 | elif (not isinstance(A, (Structure, Term))) and (not isinstance(B, (Structure, Term))): 133 | if A != B: 134 | raise UnificationFail(f"Constant Conflict {A}, {B}") 135 | else: 136 | did_a_thing = False 137 | 138 | if did_a_thing: 139 | tried.clear() 140 | else: 141 | # Add it back 142 | E.add((A, B)) 143 | tried.add((A, B)) 144 | 145 | return E 146 | 147 | 148 | class FailResult(Result): 149 | def mgu(self, dbg=NoDebugger()): 150 | dbg.output("Failure to unify was already detected") 151 | return None 152 | -------------------------------------------------------------------------------- /logicpy/structure.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | from logicpy.data import with_scope, occurences, has_occurence 5 | 6 | class Structure: 7 | # builtin operators, see below 8 | 9 | def occurences(self, O): 10 | pass 11 | 12 | def __and__(self, other): 13 | from logicpy.builtin import and_ 14 | return and_(self, other) 15 | 16 | def __or__(self, other): 17 | from logicpy.builtin import or_ 18 | return or_(self, other) 19 | 20 | def with_scope(self, scope): 21 | return self 22 | 23 | @staticmethod 24 | def scope_id(): 25 | return random.getrandbits(64) 26 | 27 | 28 | class MultiArg(Structure): 29 | def __init__(self, *args): 30 | self.args = args 31 | 32 | def __str__(self): 33 | return '(' + f" {self.op} ".join(map(str, self.args)) + ')' 34 | 35 | def __repr__(self): 36 | return type(self).__name__ + '(' + ', '.join(map(repr, self.args)) + ')' 37 | 38 | def with_scope(self, scope): 39 | args = [with_scope(a, scope) for a in self.args] 40 | return type(self)(*args) 41 | 42 | def occurences(self, O): 43 | for a in self.args: 44 | occurences(a, O) 45 | 46 | def has_occurence(self, var): 47 | return any(has_occurence(a, var) for a in self.args) 48 | 49 | 50 | class BinaryArg(MultiArg): 51 | def __init__(self, left, right): 52 | self.left = left 53 | self.right = right 54 | 55 | @property 56 | def args(self): 57 | return (self.left, self.right) 58 | 59 | 60 | class MonoArg(Structure): 61 | def __init__(self, arg): 62 | self.arg = arg 63 | 64 | def __str__(self): 65 | return f"{type(self).__name__}({self.arg})" 66 | 67 | def __repr__(self): 68 | return f"{type(self).__name__}({self.arg!r})" 69 | 70 | def with_scope(self, scope): 71 | return type(self)(with_scope(self.arg, scope)) 72 | 73 | def occurences(self, O): 74 | occurences(self.arg, O) 75 | 76 | def has_occurence(self, var): 77 | return has_occurence(self.arg, var) 78 | -------------------------------------------------------------------------------- /logicpy/tests.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from logicpy import * 5 | 6 | class UniverseAndNamespace(unittest.TestCase): 7 | def setUp(self): 8 | self.u = Universe() 9 | self.n = self.u.namespace() 10 | self.setup_universe(self.u, self.n) 11 | 12 | 13 | class Various(unittest.TestCase): 14 | def test_family(self): 15 | u, n = Universe().and_namespace() 16 | 17 | n.parent[_.alice, _.bob] = True 18 | n.parent[_.alice, _.charlie] = True 19 | n.sibling[_.X, _.Y] = n.parent(_.P, _.X) & n.parent(_.P, _.Y) & (_.X != _.Y) 20 | 21 | expected = [ 22 | {'U': _.bob, 'V': _.charlie}, 23 | {'U': _.charlie, 'V': _.bob} 24 | ] 25 | 26 | res = u.simple_query(n.sibling(_.U, _.V)) 27 | self.assertEqual(res, expected) 28 | 29 | 30 | def peano(x): 31 | if x == 0: return _.zero 32 | return _.s(peano(x-1)) 33 | 34 | 35 | def inv_peano(x): 36 | if x == _.zero: return 0 37 | return 1 + inv_peano(x.children[0]) 38 | 39 | 40 | class Peano(UniverseAndNamespace): 41 | def setup_universe(self, u, n): 42 | n.sum[_.zero, _.X, _.X] = True 43 | n.sum[_.s(_.X), _.Y, _.Z] = n.sum(_.X, _.s(_.Y), _.Z) 44 | 45 | def do_sum(self, a, b): 46 | res = self.u.simple_query(self.n.sum(peano(a), peano(b), _.X)) 47 | #print(", ".join(map(str, res))) 48 | total = inv_peano(res[0]['X']) 49 | self.assertEqual(total, a+b) 50 | 51 | def test_base(self): 52 | self.do_sum(0, 5) 53 | 54 | def test_easy(self): 55 | self.do_sum(1, 1) 56 | 57 | def test_full_accumulate(self): 58 | self.do_sum(7, 0) 59 | 60 | def test_whatever(self): 61 | self.do_sum(4, 7) 62 | 63 | 64 | def fib(x): 65 | return {0: 1, 1: 2}.get(x) or fib(x-1) + fib(x-2) 66 | 67 | 68 | class Fibonacci(UniverseAndNamespace): 69 | def setup_universe(self, u, n): 70 | n.fib[0, 1] = True 71 | n.fib[1, 2] = True 72 | n.fib[_.N, _.Res] = and_( 73 | _.N > 1, 74 | _.N1 << _.N - 1, 75 | _.N2 << _.N - 2, 76 | n.fib(_.N1, _.Res1), 77 | n.fib(_.N2, _.Res2), 78 | _.Res << _.Res1 + _.Res2) 79 | 80 | def do_fib(self, x): 81 | res = self.u.simple_query(self.n.fib(x, _.X)) 82 | self.assertEqual(len(res), 1) 83 | self.assertEqual(res[0]['X'], fib(x)) 84 | 85 | def test_basecases(self): 86 | self.do_fib(0) 87 | self.do_fib(1) 88 | 89 | def test_second(self): 90 | self.do_fib(2) 91 | 92 | def test_lots(self): 93 | for i in range(3, 7): 94 | self.do_fib(i) 95 | 96 | 97 | node = _.node 98 | empty = _.empty 99 | 100 | class BalancedTrees(UniverseAndNamespace): 101 | @classmethod 102 | def print_tree(cls, tree, level=0): 103 | if tree.name == 'node': 104 | l, val, r = tree.children 105 | print((" " * level) + str(val)) 106 | cls.print_tree(l, level+1) 107 | cls.print_tree(r, level+1) 108 | 109 | def setup_universe(self, u, n): 110 | n.depth[empty, 0] = True 111 | n.depth[node(_.L, _, _.R), _.MaxDepth] = and_( 112 | n.depth(_.L, _.Ld), 113 | n.depth(_.R, _.Rd), 114 | _.MaxDepth << 1 + max_(_.Ld, _.Rd)) 115 | 116 | n.balanced[empty] = True 117 | n.balanced[node(_.L, _, _.R)] = and_( 118 | n.balanced(_.L), 119 | n.balanced(_.R), 120 | n.depth(_.L, _.Ld), 121 | n.depth(_.R, _.Rd), 122 | 1 >= abs_(_.Ld - _.Rd)) 123 | 124 | n.add_to[empty, _.El, node(empty, _.El, empty)] = True 125 | 126 | n.add_to[node(_.L, _.V, _.R), _.El, node(_.AddedLeft, _.V, _.R)] = and_( 127 | n.depth(_.L, _.Ld), 128 | n.depth(_.R, _.Rd), 129 | _.Ld <= _.Rd, 130 | n.add_to(_.L, _.El, _.AddedLeft)) 131 | 132 | n.add_to[node(_.L, _.V, _.R), _.El, node(_.L, _.V, _.AddedRight)] = and_( 133 | n.depth(_.L, _.Ld), 134 | n.depth(_.R, _.Rd), 135 | _.Ld > _.Rd, 136 | n.add_to(_.R, _.El, _.AddedRight)) 137 | 138 | def test_equality(self): 139 | a = node(empty,3,node(empty,4,node(empty,2,empty))) 140 | b = node(empty,3,node(empty,4,node(empty,2,empty))) 141 | c = node(node(empty,7,empty),3,node(empty,4,node(empty,2,empty))) 142 | self.assertTrue(a.really_equal(b)) 143 | self.assertFalse(b.really_equal(c)) 144 | 145 | def test_add(self): 146 | inp = node(empty,3,node(empty,4,node(empty,2,empty))) 147 | out = node(node(empty,7,empty),3,node(empty,4,node(empty,2,empty))) 148 | res = self.u.simple_query(self.n.add_to(node(empty,3,node(empty,4,node(empty,2,empty))), 7, _.X))[0]['X'] 149 | self.assertTrue(res.really_equal(out)) 150 | 151 | 152 | if __name__ == '__main__': 153 | unittest.main() 154 | -------------------------------------------------------------------------------- /logicpy/util/getch.py: -------------------------------------------------------------------------------- 1 | 2 | def _find_getch(): 3 | try: 4 | import termios 5 | except ImportError: 6 | # Non-POSIX. Return msvcrt's (Windows') getch. 7 | import msvcrt 8 | return msvcrt.getch 9 | 10 | # POSIX system. Create and return a getch that manipulates the tty. 11 | import sys, tty 12 | def _getch(): 13 | fd = sys.stdin.fileno() 14 | old_settings = termios.tcgetattr(fd) 15 | try: 16 | tty.setraw(fd) 17 | ch = sys.stdin.read(1) 18 | finally: 19 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 20 | return ch 21 | 22 | return _getch 23 | 24 | getch = _find_getch() 25 | --------------------------------------------------------------------------------