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 +9 -0
- peye-0.1.0/PKG-INFO +153 -0
- peye-0.1.0/README.md +127 -0
- peye-0.1.0/peye/__init__.py +47 -0
- peye-0.1.0/peye/__main__.py +3 -0
- peye-0.1.0/peye/arith.py +118 -0
- peye-0.1.0/peye/builtins.py +193 -0
- peye-0.1.0/peye/cli.py +160 -0
- peye-0.1.0/peye/common.py +31 -0
- peye-0.1.0/peye/dsl.py +407 -0
- peye-0.1.0/peye/engine.py +572 -0
- peye-0.1.0/peye/functions.py +37 -0
- peye-0.1.0/peye/program.py +269 -0
- peye-0.1.0/peye/proof.py +508 -0
- peye-0.1.0/peye/reader.py +169 -0
- peye-0.1.0/peye/terms.py +442 -0
- peye-0.1.0/peye/writer.py +172 -0
- peye-0.1.0/peye.egg-info/PKG-INFO +153 -0
- peye-0.1.0/peye.egg-info/SOURCES.txt +31 -0
- peye-0.1.0/peye.egg-info/dependency_links.txt +1 -0
- peye-0.1.0/peye.egg-info/entry_points.txt +2 -0
- peye-0.1.0/peye.egg-info/top_level.txt +1 -0
- peye-0.1.0/pyproject.toml +29 -0
- peye-0.1.0/setup.cfg +4 -0
- peye-0.1.0/tests/test_arithmetic.py +72 -0
- peye-0.1.0/tests/test_check_report.py +103 -0
- peye-0.1.0/tests/test_cli.py +96 -0
- peye-0.1.0/tests/test_core.py +204 -0
- peye-0.1.0/tests/test_dsl.py +98 -0
- peye-0.1.0/tests/test_examples.py +72 -0
- peye-0.1.0/tests/test_proof.py +94 -0
- peye-0.1.0/tests/test_syntax.py +71 -0
- peye-0.1.0/tests/test_terms.py +71 -0
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
|
+

|
|
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
|
+

|
|
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()
|
peye-0.1.0/peye/arith.py
ADDED
|
@@ -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)
|