peye 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
peye-0.1.0/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ # MIT License
2
+
3
+ ### Copyright 2021-2026 Jos De Roo, KNoWS office of IDLab, Ghent University - imec
4
+
5
+ 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:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ 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.
peye-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: peye
3
+ Version: 0.1.0
4
+ Summary: peye — reasoning you can see. A dependency-free Python rule language with forward and backward reasoning and checkable proofs.
5
+ Author: Jos De Roo
6
+ License: # MIT License
7
+
8
+ ### Copyright 2021-2026 Jos De Roo, KNoWS office of IDLab, Ghent University - imec
9
+
10
+ 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:
11
+
12
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13
+
14
+ 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.
15
+
16
+ Project-URL: Homepage, https://github.com/eyereasoner/peye
17
+ Project-URL: Documentation, https://eyereasoner.github.io/peye/
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE.md
25
+ Dynamic: license-file
26
+
27
+ # peye
28
+
29
+ ![EYE](https://josd.github.io/images/eye.png)
30
+
31
+ *peye — reasoning you can see.*
32
+
33
+ A standalone, dependency-free **Python rule language** with forward and backward
34
+ reasoning and checkable proofs.
35
+
36
+ peye turns explicit facts and rules into conclusions whose derivations can be
37
+ inspected and checked. An answer can arrive together with a certificate, and
38
+ that certificate can be verified against the program that produced it.
39
+
40
+ Facts and rules are Python:
41
+
42
+ ```python
43
+ from peye import *
44
+
45
+ fact(human('socrates'))
46
+ forward(mortal(X), human(X))
47
+ ```
48
+
49
+ There is nothing to declare: a name the program uses without defining it is a
50
+ variable when it starts with a capital or an underscore, like `X`, and a
51
+ predicate otherwise, like `human`. Atoms are strings.
52
+
53
+ `forward(Head, *Body)` materializes conclusions until a fixpoint.
54
+ `backward(Head, *Body)` defines a predicate evaluated when called. The two
55
+ compose: a forward body may call backward definitions, and a backward goal may
56
+ use facts that forward reasoning established.
57
+
58
+ ## The thread
59
+
60
+ peye is built around one idea: **reasoning you can see**. You write facts and
61
+ rules; peye draws conclusions, forward until nothing new follows and backward
62
+ on request, and every answer can come with a proof. A separate checker verifies
63
+ that proof against the program, independently of the reasoner, and does more
64
+ than a classic proof checker:
65
+
66
+ - **The derivation:** every step is an instance of a program clause (C1),
67
+ nothing is circular (C2), and every claim and every use is justified (C4).
68
+ - **The form (C3):** every step has exactly one justification of an allowed
69
+ kind, in the right shape, so the report shows at once whether a proof fails
70
+ on form or on content.
71
+ - **Recomputation (C5):** built-in calculations are redone rather than trusted.
72
+ - **Honesty about absence:** "there is nothing that …" and "these are *all* the
73
+ answers" cannot be proved; they become explicit obligations, which the
74
+ checker tries to refute with the evidence at hand (C6).
75
+ - **The right question (C7):** the proof answers the question asked and
76
+ contains nothing beside it.
77
+
78
+ The report is itself Python data, and every generated proof is checked before it
79
+ is returned. Proofs are read with Python's `ast` module and never executed, so
80
+ checking a proof from someone else runs none of their code. Around that core, [59 examples](https://eyereasoner.github.io/peye/examples/)
81
+ grew, from Socrates and the zebra puzzle to a hospital research portal decided
82
+ under today's EU rules and under the Commission's Digital Omnibus proposal, and
83
+ package holiday cancellations under the 2015 and the revised Package Travel
84
+ Directive. Each has a [deck](https://eyereasoner.github.io/peye/examples/deck/)
85
+ for a wide audience and can be run in the [playground](https://eyereasoner.github.io/peye/playground/).
86
+
87
+ A proof guarantees that the conclusions follow from the rules, not that the
88
+ rules say what the law or the policy says. So `peye --unused` shows which
89
+ parts of a translation make no difference to the conclusions, and an expert
90
+ knows [where to look](https://eyereasoner.github.io/peye/GUIDE#checking-the-translation-not-just-the-reasoning).
91
+
92
+ ## Run it
93
+
94
+ Python 3.9 or newer. No dependencies, no build step. From the root of a
95
+ checkout:
96
+
97
+ ```sh
98
+ python -m peye examples/socrates.py
99
+ python -m peye --proof examples/socrates.py
100
+ python -m peye --proof examples/socrates.py | python -m peye --check-proof - examples/socrates.py
101
+ python -m unittest discover -s tests
102
+ ```
103
+
104
+ To use peye from anywhere, install it; an editable install keeps using the
105
+ checkout, so your edits take effect at once:
106
+
107
+ ```sh
108
+ pip install -e .
109
+ peye --proof examples/socrates.py
110
+ python examples/socrates.py --proof
111
+ ```
112
+
113
+ Once peye is installed, a program is also a script: `python program.py`
114
+ runs it with the same options as `peye program.py`. Run `peye --help` for the
115
+ full command line.
116
+
117
+ Or in the browser: the [playground](https://eyereasoner.github.io/peye/playground/) edits, runs and checks any
118
+ example, with peye running in the page through Pyodide. To run it from a
119
+ checkout, serve it (`python -m http.server`) and open `/playground/`.
120
+
121
+ ## From Python
122
+
123
+ ```python
124
+ from peye import check_proof, load_text, run
125
+
126
+ socrates = load_text("""
127
+ from peye import *
128
+ fact(human('socrates'))
129
+ forward(mortal(X), human(X))
130
+ """)
131
+ result = run(socrates, goal='mortal(X)', proof=True)
132
+ print(result.answers) # ["mortal('socrates')"]
133
+ print(check_proof(socrates, result.proof)['valid']) # True
134
+ ```
135
+
136
+ `load('program.py')` reads a program file the same way.
137
+
138
+ ## Read on
139
+
140
+ - **[Make reasoning something you can see](https://eyereasoner.github.io/peye/GUIDE)** —
141
+ what the language is for, how to write it, what a checked proof does and does
142
+ not establish, and how the engine works.
143
+ - **[Examples](https://eyereasoner.github.io/peye/examples/)** — 59 complete programs, each with its saved
144
+ conclusions, proof and C1-C7 check report.
145
+ - **[Example decks](https://eyereasoner.github.io/peye/examples/deck/)** — a short card deck for every
146
+ example, explaining it for a wide audience: the question, what peye
147
+ concludes, why, and what the proof checker confirms.
148
+ - **[Playground](https://eyereasoner.github.io/peye/playground/)** — write a program in the browser, run it, check
149
+ its proof, and share a link to exactly what you see.
150
+
151
+ ## License
152
+
153
+ [MIT](https://eyereasoner.github.io/peye/LICENSE.md)
peye-0.1.0/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # peye
2
+
3
+ ![EYE](https://josd.github.io/images/eye.png)
4
+
5
+ *peye — reasoning you can see.*
6
+
7
+ A standalone, dependency-free **Python rule language** with forward and backward
8
+ reasoning and checkable proofs.
9
+
10
+ peye turns explicit facts and rules into conclusions whose derivations can be
11
+ inspected and checked. An answer can arrive together with a certificate, and
12
+ that certificate can be verified against the program that produced it.
13
+
14
+ Facts and rules are Python:
15
+
16
+ ```python
17
+ from peye import *
18
+
19
+ fact(human('socrates'))
20
+ forward(mortal(X), human(X))
21
+ ```
22
+
23
+ There is nothing to declare: a name the program uses without defining it is a
24
+ variable when it starts with a capital or an underscore, like `X`, and a
25
+ predicate otherwise, like `human`. Atoms are strings.
26
+
27
+ `forward(Head, *Body)` materializes conclusions until a fixpoint.
28
+ `backward(Head, *Body)` defines a predicate evaluated when called. The two
29
+ compose: a forward body may call backward definitions, and a backward goal may
30
+ use facts that forward reasoning established.
31
+
32
+ ## The thread
33
+
34
+ peye is built around one idea: **reasoning you can see**. You write facts and
35
+ rules; peye draws conclusions, forward until nothing new follows and backward
36
+ on request, and every answer can come with a proof. A separate checker verifies
37
+ that proof against the program, independently of the reasoner, and does more
38
+ than a classic proof checker:
39
+
40
+ - **The derivation:** every step is an instance of a program clause (C1),
41
+ nothing is circular (C2), and every claim and every use is justified (C4).
42
+ - **The form (C3):** every step has exactly one justification of an allowed
43
+ kind, in the right shape, so the report shows at once whether a proof fails
44
+ on form or on content.
45
+ - **Recomputation (C5):** built-in calculations are redone rather than trusted.
46
+ - **Honesty about absence:** "there is nothing that …" and "these are *all* the
47
+ answers" cannot be proved; they become explicit obligations, which the
48
+ checker tries to refute with the evidence at hand (C6).
49
+ - **The right question (C7):** the proof answers the question asked and
50
+ contains nothing beside it.
51
+
52
+ The report is itself Python data, and every generated proof is checked before it
53
+ is returned. Proofs are read with Python's `ast` module and never executed, so
54
+ checking a proof from someone else runs none of their code. Around that core, [59 examples](https://eyereasoner.github.io/peye/examples/)
55
+ grew, from Socrates and the zebra puzzle to a hospital research portal decided
56
+ under today's EU rules and under the Commission's Digital Omnibus proposal, and
57
+ package holiday cancellations under the 2015 and the revised Package Travel
58
+ Directive. Each has a [deck](https://eyereasoner.github.io/peye/examples/deck/)
59
+ for a wide audience and can be run in the [playground](https://eyereasoner.github.io/peye/playground/).
60
+
61
+ A proof guarantees that the conclusions follow from the rules, not that the
62
+ rules say what the law or the policy says. So `peye --unused` shows which
63
+ parts of a translation make no difference to the conclusions, and an expert
64
+ knows [where to look](https://eyereasoner.github.io/peye/GUIDE#checking-the-translation-not-just-the-reasoning).
65
+
66
+ ## Run it
67
+
68
+ Python 3.9 or newer. No dependencies, no build step. From the root of a
69
+ checkout:
70
+
71
+ ```sh
72
+ python -m peye examples/socrates.py
73
+ python -m peye --proof examples/socrates.py
74
+ python -m peye --proof examples/socrates.py | python -m peye --check-proof - examples/socrates.py
75
+ python -m unittest discover -s tests
76
+ ```
77
+
78
+ To use peye from anywhere, install it; an editable install keeps using the
79
+ checkout, so your edits take effect at once:
80
+
81
+ ```sh
82
+ pip install -e .
83
+ peye --proof examples/socrates.py
84
+ python examples/socrates.py --proof
85
+ ```
86
+
87
+ Once peye is installed, a program is also a script: `python program.py`
88
+ runs it with the same options as `peye program.py`. Run `peye --help` for the
89
+ full command line.
90
+
91
+ Or in the browser: the [playground](https://eyereasoner.github.io/peye/playground/) edits, runs and checks any
92
+ example, with peye running in the page through Pyodide. To run it from a
93
+ checkout, serve it (`python -m http.server`) and open `/playground/`.
94
+
95
+ ## From Python
96
+
97
+ ```python
98
+ from peye import check_proof, load_text, run
99
+
100
+ socrates = load_text("""
101
+ from peye import *
102
+ fact(human('socrates'))
103
+ forward(mortal(X), human(X))
104
+ """)
105
+ result = run(socrates, goal='mortal(X)', proof=True)
106
+ print(result.answers) # ["mortal('socrates')"]
107
+ print(check_proof(socrates, result.proof)['valid']) # True
108
+ ```
109
+
110
+ `load('program.py')` reads a program file the same way.
111
+
112
+ ## Read on
113
+
114
+ - **[Make reasoning something you can see](https://eyereasoner.github.io/peye/GUIDE)** —
115
+ what the language is for, how to write it, what a checked proof does and does
116
+ not establish, and how the engine works.
117
+ - **[Examples](https://eyereasoner.github.io/peye/examples/)** — 59 complete programs, each with its saved
118
+ conclusions, proof and C1-C7 check report.
119
+ - **[Example decks](https://eyereasoner.github.io/peye/examples/deck/)** — a short card deck for every
120
+ example, explaining it for a wide audience: the question, what peye
121
+ concludes, why, and what the proof checker confirms.
122
+ - **[Playground](https://eyereasoner.github.io/peye/playground/)** — write a program in the browser, run it, check
123
+ its proof, and share a link to exactly what you see.
124
+
125
+ ## License
126
+
127
+ [MIT](https://eyereasoner.github.io/peye/LICENSE.md)
@@ -0,0 +1,47 @@
1
+ """peye: reasoning you can see, in Python.
2
+
3
+ Facts and rules are Python. Forward rules materialize conclusions until a
4
+ fixpoint, backward rules are decided when a goal asks for them, and every
5
+ answer can come with a proof that a separate checker verifies against the
6
+ program, condition by condition (C1-C7).
7
+ """
8
+ import sys
9
+
10
+ from .dsl import (
11
+ _, arg, atom_chars, atom_codes, atom_concat, atom_length, backward, call, compare,
12
+ contradiction, eq, fact, facts_from, fail, false, findall, forward, functor, identical, is_,
13
+ is_atom, is_compound, is_float, is_ground, is_int, is_nonvar, is_number, is_var, load,
14
+ load_text, ne, not_, not_identical, not_unify, once, preds, build, query, struct, true,
15
+ unify, univ, vars,
16
+ )
17
+ from .engine import Result, run, unused_clauses
18
+ from .program import Program
19
+ from .proof import check_proof, check_report, public_report, verdict_text
20
+ from .reader import read_term, read_terms
21
+ from .terms import PeyeError, Struct, Var
22
+ from .writer import write
23
+
24
+ # Exact integers can be far longer than Python prints by default.
25
+ if hasattr(sys, 'set_int_max_str_digits'):
26
+ sys.set_int_max_str_digits(0)
27
+
28
+ __version__ = '0.1.0'
29
+
30
+ # What `from peye import *` gives a program: the names it states clauses
31
+ # with. The library API (load, run, check_proof, ...) is imported by name.
32
+ __all__ = [
33
+ # Stating a program.
34
+ 'preds', 'vars', '_', 'fact', 'facts_from', 'forward', 'backward', 'query', 'contradiction',
35
+ 'struct',
36
+ # Controls.
37
+ 'call', 'once', 'not_', 'findall',
38
+ # Primitives.
39
+ 'true', 'fail', 'false', 'unify', 'not_unify', 'identical', 'not_identical', 'compare',
40
+ 'is_', 'eq', 'ne', 'is_var', 'is_nonvar', 'is_ground', 'is_atom', 'is_number', 'is_int',
41
+ 'is_float', 'is_compound', 'functor', 'arg', 'univ', 'atom_chars', 'atom_codes',
42
+ 'atom_length', 'atom_concat',
43
+ ]
44
+
45
+ from .dsl import run_as_script # noqa: E402
46
+
47
+ run_as_script()
@@ -0,0 +1,3 @@
1
+ from .cli import entry
2
+
3
+ entry()
@@ -0,0 +1,118 @@
1
+ """Arithmetic evaluation with Python semantics.
2
+
3
+ An expression term means what the same expression means in Python: integers
4
+ are unbounded and exact, floats are IEEE-754 doubles, ``/`` is true division,
5
+ ``//`` and ``%`` are floor division and modulo, ``**`` is exact on integers
6
+ with a nonnegative exponent, and ``&``, ``|``, ``^``, ``~``, ``<<`` and ``>>``
7
+ are the bitwise operators. The functions are Python's builtins ``abs``,
8
+ ``min``, ``max``, ``round``, ``int`` and ``float``, and from ``math`` the
9
+ functions listed in FUNCTIONS. A result that is not a finite real number is an
10
+ error, never a value.
11
+ """
12
+ import math
13
+
14
+ from .terms import PeyeError, Var, deref
15
+
16
+ # A power whose result would need more bits than this is refused rather than
17
+ # allowed to exhaust memory.
18
+ MAX_POWER_BITS = 1 << 26
19
+
20
+
21
+ def _power(a, b):
22
+ if type(a) is int and type(b) is int and b > 0 and abs(a) > 1:
23
+ if b * a.bit_length() > MAX_POWER_BITS:
24
+ raise PeyeError(f'arithmetic resource: {a} ** {b} is too large')
25
+ return a ** b
26
+
27
+
28
+ def _shift(a, b):
29
+ if type(a) is int and type(b) is int and b > MAX_POWER_BITS:
30
+ raise PeyeError(f'arithmetic resource: {a} << {b} is too large')
31
+ return a << b
32
+
33
+
34
+ BINARY = {
35
+ '+': lambda a, b: a + b,
36
+ '-': lambda a, b: a - b,
37
+ '*': lambda a, b: a * b,
38
+ '/': lambda a, b: a / b,
39
+ '//': lambda a, b: a // b,
40
+ '%': lambda a, b: a % b,
41
+ '**': _power,
42
+ '<<': _shift,
43
+ '>>': lambda a, b: a >> b,
44
+ '^': lambda a, b: a ^ b,
45
+ ',': lambda a, b: a & b,
46
+ ';': lambda a, b: a | b,
47
+ }
48
+ UNARY = {
49
+ '-': lambda a: -a,
50
+ '+': lambda a: +a,
51
+ '~': lambda a: ~a,
52
+ }
53
+ FUNCTIONS = {
54
+ 'abs': abs, 'min': min, 'max': max, 'round': round, 'int': int, 'float': float,
55
+ 'pow': _power, 'gcd': math.gcd, 'lcm': getattr(math, 'lcm', None),
56
+ 'floor': math.floor, 'ceil': math.ceil, 'trunc': math.trunc,
57
+ 'sqrt': math.sqrt, 'isqrt': math.isqrt, 'exp': math.exp, 'log': math.log,
58
+ 'log2': math.log2, 'log10': math.log10,
59
+ 'sin': math.sin, 'cos': math.cos, 'tan': math.tan,
60
+ 'asin': math.asin, 'acos': math.acos, 'atan': math.atan, 'atan2': math.atan2,
61
+ 'sinh': math.sinh, 'cosh': math.cosh, 'tanh': math.tanh,
62
+ 'hypot': math.hypot, 'degrees': math.degrees, 'radians': math.radians,
63
+ 'fmod': math.fmod, 'copysign': math.copysign,
64
+ }
65
+ FUNCTIONS = {name: fn for name, fn in FUNCTIONS.items() if fn is not None}
66
+ CONSTANTS = {'pi': math.pi, 'e': math.e, 'tau': math.tau}
67
+
68
+
69
+ def evaluate(term, env):
70
+ term = deref(term, env)
71
+ kind = type(term)
72
+ if kind is int or kind is float:
73
+ return term
74
+ if kind is Var:
75
+ raise PeyeError('arithmetic needs a bound expression, found an unbound variable')
76
+ if kind is str:
77
+ if term in CONSTANTS:
78
+ return CONSTANTS[term]
79
+ raise PeyeError(f'{term!r} is not an arithmetic value')
80
+ values = [evaluate(arg, env) for arg in term.args]
81
+ return apply(term.name, values)
82
+
83
+
84
+ def apply(name, values):
85
+ arity = len(values)
86
+ if arity == 2 and name in BINARY:
87
+ fn = BINARY[name]
88
+ elif arity == 1 and name in UNARY:
89
+ fn = UNARY[name]
90
+ elif name in FUNCTIONS:
91
+ fn = FUNCTIONS[name]
92
+ else:
93
+ raise PeyeError(f'{name}/{arity} is not an arithmetic function')
94
+ try:
95
+ result = fn(*values)
96
+ except ZeroDivisionError:
97
+ raise PeyeError(f'arithmetic: division by zero in {name}') from None
98
+ except OverflowError:
99
+ raise PeyeError(f'arithmetic: overflow in {name}') from None
100
+ except (ValueError, TypeError) as error:
101
+ raise PeyeError(f'arithmetic: {name} {error}') from None
102
+ kind = type(result)
103
+ if kind is int:
104
+ return result
105
+ if kind is float:
106
+ if not math.isfinite(result):
107
+ raise PeyeError(f'arithmetic: {name} has no finite result')
108
+ return result
109
+ if kind is bool:
110
+ return int(result)
111
+ raise PeyeError(f'arithmetic: {name} has no real result')
112
+
113
+
114
+ def compare(left, right, env):
115
+ """-1, 0 or 1. Python compares integers and floats exactly."""
116
+ a = evaluate(left, env)
117
+ b = evaluate(right, env)
118
+ return -1 if a < b else (1 if a > b else 0)
@@ -0,0 +1,193 @@
1
+ """The pure primitives.
2
+
3
+ Only pure primitives belong here. Collections, call and negation are controls
4
+ handled by the solver and explicitly distinguished in proof documents.
5
+
6
+ A test decides a goal at most once: it is handed the goal's arguments, the
7
+ same arguments dereferenced, the substitution and a bind function, and says
8
+ whether the goal holds. Any bindings it made on the way to failing are undone
9
+ for it. A relation can hold several times: it is a generator that yields once
10
+ per solution, and the solver undoes the bindings of one solution before asking
11
+ for the next.
12
+ """
13
+ import itertools
14
+
15
+ from . import arith
16
+ from .terms import (
17
+ PeyeError, Struct, Var, compare_terms, copy_resolved, deref, is_ground,
18
+ key, list_from_items, proper_list_items, unify,
19
+ )
20
+
21
+
22
+ def _comparison(holds):
23
+ return lambda args, resolved, env, bind: holds(arith.compare(args[0], args[1], env))
24
+
25
+
26
+ def _same(args, env):
27
+ return compare_terms(copy_resolved(args[0], env), copy_resolved(args[1], env)) == 0
28
+
29
+
30
+ def _not_unify(args, resolved, env, bind):
31
+ mark = env.mark()
32
+ unifies = unify(args[0], args[1], env)
33
+ env.undo(mark)
34
+ return not unifies
35
+
36
+
37
+ def _bounded_integer(term, low, high):
38
+ if type(term) is not int or term < 0:
39
+ raise PeyeError('expected a nonnegative integer')
40
+ if term < low or term > high:
41
+ raise PeyeError(f'integer outside {low}..{high}')
42
+ return term
43
+
44
+
45
+ _functor_serial = itertools.count(1)
46
+
47
+
48
+ def _functor(args, resolved, env, bind):
49
+ term, name, arity = resolved
50
+ if type(term) is not Var:
51
+ if type(term) is Struct:
52
+ return bind(args[1], term.name) and bind(args[2], len(term.args))
53
+ return bind(args[1], term) and bind(args[2], 0)
54
+ count = _bounded_integer(arity, 0, 1024)
55
+ if type(name) is Var:
56
+ raise PeyeError('functor/3 needs a bound name')
57
+ if count > 0 and type(name) is not str:
58
+ raise PeyeError('functor/3 needs an atom name')
59
+ # Unique names across primitive invocations preserve variable isolation.
60
+ serial = next(_functor_serial)
61
+ if count == 0:
62
+ return bind(args[0], name)
63
+ return bind(args[0], Struct(name, tuple(Var(f'_functor{serial}_{i}') for i in range(count))))
64
+
65
+
66
+ def _arg(args, resolved, env, bind):
67
+ position, term = resolved[0], resolved[1]
68
+ index = _bounded_integer(position, 1, 1024)
69
+ return type(term) is Struct and index <= len(term.args) and bind(args[2], term.args[index - 1])
70
+
71
+
72
+ def _univ(args, resolved, env, bind):
73
+ term = resolved[0]
74
+ if type(term) is not Var:
75
+ if type(term) is Struct:
76
+ return bind(args[1], list_from_items([term.name, *term.args]))
77
+ return bind(args[1], list_from_items([term]))
78
+ items = proper_list_items(args[1], env)
79
+ items = None if items is None else [deref(item, env) for item in items]
80
+ if not items or type(items[0]) is Var or (len(items) > 1 and type(items[0]) is not str):
81
+ raise PeyeError('univ/2 needs a nonempty bound term list')
82
+ return bind(args[0], items[0] if len(items) == 1 else Struct(items[0], tuple(items[1:])))
83
+
84
+
85
+ def _atom_text(name, codes):
86
+ def test(args, resolved, env, bind):
87
+ term = resolved[0]
88
+ if type(term) is str:
89
+ items = [ord(ch) for ch in term] if codes else list(term)
90
+ return bind(args[1], list_from_items(items))
91
+ items = proper_list_items(args[1], env)
92
+ if items is None:
93
+ raise PeyeError(f'{name} needs a proper list')
94
+ chars = []
95
+ for item in items:
96
+ item = deref(item, env)
97
+ if codes:
98
+ chars.append(chr(_bounded_integer(item, 0, 0x10FFFF)))
99
+ else:
100
+ if type(item) is not str or len(item) != 1:
101
+ raise PeyeError('atom_chars/2 needs characters')
102
+ chars.append(item)
103
+ return bind(args[0], ''.join(chars))
104
+ return test
105
+
106
+
107
+ def _atom_length(args, resolved, env, bind):
108
+ if type(resolved[0]) is not str:
109
+ raise PeyeError('atom_length/2 needs a bound atom')
110
+ return bind(args[1], len(resolved[0]))
111
+
112
+
113
+ def _compare(args, resolved, env, bind):
114
+ order = compare_terms(copy_resolved(args[1], env), copy_resolved(args[2], env))
115
+ return bind(args[0], '<' if order < 0 else ('>' if order > 0 else '='))
116
+
117
+
118
+ def _is(args, resolved, env, bind):
119
+ return bind(args[0], arith.evaluate(args[1], env))
120
+
121
+
122
+ TESTS = {
123
+ ('true', 0): lambda args, resolved, env, bind: True,
124
+ ('fail', 0): lambda args, resolved, env, bind: False,
125
+ ('false', 0): lambda args, resolved, env, bind: False,
126
+ ('unify', 2): lambda args, resolved, env, bind: bind(args[0], args[1]),
127
+ ('not_unify', 2): _not_unify,
128
+ ('identical', 2): lambda args, resolved, env, bind: _same(args, env),
129
+ ('not_identical', 2): lambda args, resolved, env, bind: not _same(args, env),
130
+ ('is_', 2): _is,
131
+ ('eq', 2): _comparison(lambda n: n == 0),
132
+ ('ne', 2): _comparison(lambda n: n != 0),
133
+ ('<', 2): _comparison(lambda n: n < 0),
134
+ ('<=', 2): _comparison(lambda n: n <= 0),
135
+ ('>', 2): _comparison(lambda n: n > 0),
136
+ ('>=', 2): _comparison(lambda n: n >= 0),
137
+ ('is_var', 1): lambda args, resolved, env, bind: type(resolved[0]) is Var,
138
+ ('is_nonvar', 1): lambda args, resolved, env, bind: type(resolved[0]) is not Var,
139
+ ('is_ground', 1): lambda args, resolved, env, bind: is_ground(args[0], env),
140
+ ('is_atom', 1): lambda args, resolved, env, bind: type(resolved[0]) is str,
141
+ ('is_number', 1): lambda args, resolved, env, bind: type(resolved[0]) in (int, float),
142
+ ('is_int', 1): lambda args, resolved, env, bind: type(resolved[0]) is int,
143
+ ('is_float', 1): lambda args, resolved, env, bind: type(resolved[0]) is float,
144
+ ('is_compound', 1): lambda args, resolved, env, bind: type(resolved[0]) is Struct,
145
+ ('functor', 3): _functor,
146
+ ('arg', 3): _arg,
147
+ ('univ', 2): _univ,
148
+ ('atom_chars', 2): _atom_text('atom_chars/2', False),
149
+ ('atom_codes', 2): _atom_text('atom_codes/2', True),
150
+ ('atom_length', 2): _atom_length,
151
+ ('compare', 3): _compare,
152
+ }
153
+
154
+
155
+ def _atom_concat(args, resolved, env, mark):
156
+ left, right, whole = resolved
157
+ if type(left) is str and type(right) is str:
158
+ if unify(args[2], left + right, env):
159
+ yield env
160
+ else:
161
+ env.undo(mark)
162
+ return
163
+ if type(whole) is not str:
164
+ raise PeyeError('atom_concat/3 needs the result or both operands bound')
165
+ for i in range(len(whole) + 1):
166
+ if unify(args[0], whole[:i], env) and unify(args[1], whole[i:], env):
167
+ yield env
168
+ env.undo(mark)
169
+
170
+
171
+ RELATIONS = {
172
+ ('atom_concat', 3): _atom_concat,
173
+ }
174
+ PRIMITIVE_KEYS = frozenset(TESTS) | frozenset(RELATIONS)
175
+
176
+
177
+ def primitive(goal, env):
178
+ """Solve a primitive goal: a generator yielding env once per solution."""
179
+ goal_key = key(goal)
180
+ args = goal.args if type(goal) is Struct else ()
181
+ resolved = [deref(arg, env) for arg in args]
182
+ mark = env.mark()
183
+ test = TESTS.get(goal_key)
184
+ if test is None:
185
+ relation = RELATIONS.get(goal_key)
186
+ if relation is None:
187
+ raise PeyeError(f'unsupported primitive {goal_key[0]}/{goal_key[1]}')
188
+ yield from relation(args, resolved, env, mark)
189
+ return
190
+ if test(args, resolved, env, lambda a, b: unify(a, b, env)):
191
+ yield env
192
+ else:
193
+ env.undo(mark)