tiny-datalog 0.1.1__tar.gz → 0.2.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.
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/PKG-INFO +44 -10
- tiny_datalog-0.1.1/tiny_datalog.egg-info/PKG-INFO → tiny_datalog-0.2.0/README.md +22 -21
- tiny_datalog-0.2.0/pyproject.toml +57 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/__init__.py +3 -2
- tiny_datalog-0.2.0/tiny_datalog/defeasible.py +462 -0
- tiny_datalog-0.1.1/README.md → tiny_datalog-0.2.0/tiny_datalog.egg-info/PKG-INFO +55 -9
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/SOURCES.txt +1 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/entry_points.txt +1 -0
- tiny_datalog-0.1.1/pyproject.toml +0 -29
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/LICENSE +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/setup.cfg +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/containment.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/datalog.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/incremental.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/magic.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/prolog.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/semantics.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/semiring.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/subsumption.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/tabling.py +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/dependency_links.txt +0 -0
- {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiny-datalog
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Datalog engine small enough to read in an afternoon
|
|
5
5
|
Author: Andrew Goodchild
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/andrewgoodchild/tiny-datalog
|
|
8
|
+
Project-URL: Source, https://github.com/andrewgoodchild/tiny-datalog
|
|
9
|
+
Project-URL: Issues, https://github.com/andrewgoodchild/tiny-datalog/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/andrewgoodchild/tiny-datalog/releases
|
|
11
|
+
Project-URL: Lessons, https://github.com/andrewgoodchild/tiny-datalog/tree/main/lessons
|
|
12
|
+
Keywords: datalog,logic programming,deductive database,semi-naive evaluation,magic sets,provenance,stable models,defeasible logic,teaching
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Database
|
|
26
|
+
Classifier: Topic :: Education
|
|
27
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
8
29
|
Requires-Python: >=3.9
|
|
9
30
|
Description-Content-Type: text/markdown
|
|
10
31
|
License-File: LICENSE
|
|
@@ -105,7 +126,7 @@ Nothing to install:
|
|
|
105
126
|
|
|
106
127
|
```sh
|
|
107
128
|
git clone https://github.com/andrewgoodchild/tiny-datalog && cd tiny-datalog
|
|
108
|
-
python3 tests.py #
|
|
129
|
+
python3 tests.py # 246 tests, ~12s
|
|
109
130
|
```
|
|
110
131
|
|
|
111
132
|
## Why the language choice decides what you can ask later
|
|
@@ -210,6 +231,15 @@ tabled evaluation all agree, and that incremental maintenance matches
|
|
|
210
231
|
recomputation under random updates. 400 programs per run;
|
|
211
232
|
`TINY_DATALOG_FUZZ=3000 python3 tests.py DifferentialFuzzTests` soaks.
|
|
212
233
|
|
|
234
|
+
Answers are also checked against engines this repository did not write.
|
|
235
|
+
[`conformance/`](https://github.com/andrewgoodchild/tiny-datalog/tree/main/conformance)
|
|
236
|
+
runs the [datalog-conformance](https://pypi.org/project/datalog-conformance/)
|
|
237
|
+
corpus, harvested from Soufflé, Nemo and Crepe, through all four
|
|
238
|
+
strategies, and its defeasible theories, from SPINdle and the papers,
|
|
239
|
+
through `defeasible.py`: 195 cases pass. Every skip is named with its
|
|
240
|
+
reason: arithmetic and 10⁵-fact joins, which this course omits on
|
|
241
|
+
purpose, or cases written for a different logic.
|
|
242
|
+
|
|
213
243
|
## Learning Datalog
|
|
214
244
|
|
|
215
245
|
`lessons/` is a complete course, no prior exposure assumed, every
|
|
@@ -218,7 +248,8 @@ the current research threads. The field's own recent lecture notes
|
|
|
218
248
|
observe that the literature advises people building Datalog engines
|
|
219
249
|
better than people trying to *use* one; this course does both halves
|
|
220
250
|
on purpose — sixteen lessons where the engine is the explanation, then
|
|
221
|
-
a lesson on authoring rules that survive review
|
|
251
|
+
a lesson on authoring rules that survive review, and one on a logic
|
|
252
|
+
built for rules with exceptions. And it is built to be
|
|
222
253
|
inherited: `git clone`, no dependencies, no hosted anything, and every
|
|
223
254
|
quoted transcript re-verified by CI — the exercises cannot rot. (For where each
|
|
224
255
|
technique ships — CodeQL, RDFox, Feldera, SNOMED and the rest —
|
|
@@ -299,7 +330,7 @@ for service, cve in sorted(engine.rels["exposed"]):
|
|
|
299
330
|
|
|
300
331
|
The command-line interface installs too, as `tiny-datalog` (and
|
|
301
332
|
`tiny-datalog-semiring`, `-tabling`, `-incremental`, `-subsumption`,
|
|
302
|
-
`-containment`, `-prolog` for the satellites):
|
|
333
|
+
`-containment`, `-defeasible`, `-prolog` for the satellites):
|
|
303
334
|
|
|
304
335
|
```sh
|
|
305
336
|
tiny-datalog -q 'exposed(S, C)' supply-chain.dl
|
|
@@ -323,15 +354,18 @@ tiny_datalog/ the engine and its satellites — the code you read:
|
|
|
323
354
|
tabling.py tabled top-down evaluation (iterative QSQR)
|
|
324
355
|
subsumption.py KL-ONE-style EL classifier, compiled to Datalog
|
|
325
356
|
containment.py query containment and minimisation by homomorphism
|
|
357
|
+
defeasible.py defeasible logic: exceptions, priorities, defeaters
|
|
326
358
|
*.py three-line launchers, so `python3 datalog.py ...` works
|
|
327
359
|
straight from a checkout with nothing installed
|
|
328
360
|
programs/ teaching programs, numbered by the lesson that uses
|
|
329
361
|
them (00-* are the README's examples)
|
|
330
|
-
lessons/ getting started, glossary, and lessons 0–
|
|
362
|
+
lessons/ getting started, glossary, and lessons 0–19
|
|
331
363
|
exercises/ worked answers, verified by the test suite
|
|
332
364
|
cases/ golden test cases — add one without writing Python
|
|
365
|
+
conformance/ the external datalog-conformance corpus (Soufflé, Nemo,
|
|
366
|
+
Crepe, SPINdle), run against every evaluation strategy
|
|
333
367
|
benchmarks/ scaled input generators (chain/tree/clique/grid)
|
|
334
|
-
tests.py
|
|
368
|
+
tests.py 246 tests: every shipped program and exercise answer is
|
|
335
369
|
executed, a conformance suite runs every query through
|
|
336
370
|
every applicable strategy, and a seeded fuzzer checks
|
|
337
371
|
the same property on random programs
|
|
@@ -345,13 +379,13 @@ used.
|
|
|
345
379
|
### How big is it, honestly
|
|
346
380
|
|
|
347
381
|
The evaluator is about 850 lines (`tiny_datalog/datalog.py`, up to the
|
|
348
|
-
command-line interface), the CLI, `--explain` and why-not another 600, and the
|
|
349
|
-
2,
|
|
382
|
+
command-line interface), the CLI, `--explain` and why-not another 600, and the nine satellite modules about
|
|
383
|
+
2,900. Call it 4.3k lines of toolkit and 2.7k of tests, roughly a
|
|
350
384
|
quarter of it commentary.
|
|
351
385
|
|
|
352
386
|
"Tiny" is a claim about the evaluator, and about each satellite module
|
|
353
|
-
singly: none of the
|
|
354
|
-
the repository, which is
|
|
387
|
+
singly: none of the nine exceeds 500 lines, which a test asserts. It is not a claim about
|
|
388
|
+
the repository, which is ten modules because it teaches ten things.
|
|
355
389
|
|
|
356
390
|
There is no dead code to golf away (checked); shrinking further means
|
|
357
391
|
deleting either a technique or an explanation.
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tiny-datalog
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: A Datalog engine small enough to read in an afternoon
|
|
5
|
-
Author: Andrew Goodchild
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/andrewgoodchild/tiny-datalog
|
|
8
|
-
Requires-Python: >=3.9
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Dynamic: license-file
|
|
12
|
-
|
|
13
1
|
# tiny-datalog
|
|
14
2
|
|
|
15
3
|
[](https://github.com/andrewgoodchild/tiny-datalog/actions/workflows/ci.yml)
|
|
@@ -105,7 +93,7 @@ Nothing to install:
|
|
|
105
93
|
|
|
106
94
|
```sh
|
|
107
95
|
git clone https://github.com/andrewgoodchild/tiny-datalog && cd tiny-datalog
|
|
108
|
-
python3 tests.py #
|
|
96
|
+
python3 tests.py # 246 tests, ~12s
|
|
109
97
|
```
|
|
110
98
|
|
|
111
99
|
## Why the language choice decides what you can ask later
|
|
@@ -210,6 +198,15 @@ tabled evaluation all agree, and that incremental maintenance matches
|
|
|
210
198
|
recomputation under random updates. 400 programs per run;
|
|
211
199
|
`TINY_DATALOG_FUZZ=3000 python3 tests.py DifferentialFuzzTests` soaks.
|
|
212
200
|
|
|
201
|
+
Answers are also checked against engines this repository did not write.
|
|
202
|
+
[`conformance/`](https://github.com/andrewgoodchild/tiny-datalog/tree/main/conformance)
|
|
203
|
+
runs the [datalog-conformance](https://pypi.org/project/datalog-conformance/)
|
|
204
|
+
corpus, harvested from Soufflé, Nemo and Crepe, through all four
|
|
205
|
+
strategies, and its defeasible theories, from SPINdle and the papers,
|
|
206
|
+
through `defeasible.py`: 195 cases pass. Every skip is named with its
|
|
207
|
+
reason: arithmetic and 10⁵-fact joins, which this course omits on
|
|
208
|
+
purpose, or cases written for a different logic.
|
|
209
|
+
|
|
213
210
|
## Learning Datalog
|
|
214
211
|
|
|
215
212
|
`lessons/` is a complete course, no prior exposure assumed, every
|
|
@@ -218,7 +215,8 @@ the current research threads. The field's own recent lecture notes
|
|
|
218
215
|
observe that the literature advises people building Datalog engines
|
|
219
216
|
better than people trying to *use* one; this course does both halves
|
|
220
217
|
on purpose — sixteen lessons where the engine is the explanation, then
|
|
221
|
-
a lesson on authoring rules that survive review
|
|
218
|
+
a lesson on authoring rules that survive review, and one on a logic
|
|
219
|
+
built for rules with exceptions. And it is built to be
|
|
222
220
|
inherited: `git clone`, no dependencies, no hosted anything, and every
|
|
223
221
|
quoted transcript re-verified by CI — the exercises cannot rot. (For where each
|
|
224
222
|
technique ships — CodeQL, RDFox, Feldera, SNOMED and the rest —
|
|
@@ -299,7 +297,7 @@ for service, cve in sorted(engine.rels["exposed"]):
|
|
|
299
297
|
|
|
300
298
|
The command-line interface installs too, as `tiny-datalog` (and
|
|
301
299
|
`tiny-datalog-semiring`, `-tabling`, `-incremental`, `-subsumption`,
|
|
302
|
-
`-containment`, `-prolog` for the satellites):
|
|
300
|
+
`-containment`, `-defeasible`, `-prolog` for the satellites):
|
|
303
301
|
|
|
304
302
|
```sh
|
|
305
303
|
tiny-datalog -q 'exposed(S, C)' supply-chain.dl
|
|
@@ -323,15 +321,18 @@ tiny_datalog/ the engine and its satellites — the code you read:
|
|
|
323
321
|
tabling.py tabled top-down evaluation (iterative QSQR)
|
|
324
322
|
subsumption.py KL-ONE-style EL classifier, compiled to Datalog
|
|
325
323
|
containment.py query containment and minimisation by homomorphism
|
|
324
|
+
defeasible.py defeasible logic: exceptions, priorities, defeaters
|
|
326
325
|
*.py three-line launchers, so `python3 datalog.py ...` works
|
|
327
326
|
straight from a checkout with nothing installed
|
|
328
327
|
programs/ teaching programs, numbered by the lesson that uses
|
|
329
328
|
them (00-* are the README's examples)
|
|
330
|
-
lessons/ getting started, glossary, and lessons 0–
|
|
329
|
+
lessons/ getting started, glossary, and lessons 0–19
|
|
331
330
|
exercises/ worked answers, verified by the test suite
|
|
332
331
|
cases/ golden test cases — add one without writing Python
|
|
332
|
+
conformance/ the external datalog-conformance corpus (Soufflé, Nemo,
|
|
333
|
+
Crepe, SPINdle), run against every evaluation strategy
|
|
333
334
|
benchmarks/ scaled input generators (chain/tree/clique/grid)
|
|
334
|
-
tests.py
|
|
335
|
+
tests.py 246 tests: every shipped program and exercise answer is
|
|
335
336
|
executed, a conformance suite runs every query through
|
|
336
337
|
every applicable strategy, and a seeded fuzzer checks
|
|
337
338
|
the same property on random programs
|
|
@@ -345,13 +346,13 @@ used.
|
|
|
345
346
|
### How big is it, honestly
|
|
346
347
|
|
|
347
348
|
The evaluator is about 850 lines (`tiny_datalog/datalog.py`, up to the
|
|
348
|
-
command-line interface), the CLI, `--explain` and why-not another 600, and the
|
|
349
|
-
2,
|
|
349
|
+
command-line interface), the CLI, `--explain` and why-not another 600, and the nine satellite modules about
|
|
350
|
+
2,900. Call it 4.3k lines of toolkit and 2.7k of tests, roughly a
|
|
350
351
|
quarter of it commentary.
|
|
351
352
|
|
|
352
353
|
"Tiny" is a claim about the evaluator, and about each satellite module
|
|
353
|
-
singly: none of the
|
|
354
|
-
the repository, which is
|
|
354
|
+
singly: none of the nine exceeds 500 lines, which a test asserts. It is not a claim about
|
|
355
|
+
the repository, which is ten modules because it teaches ten things.
|
|
355
356
|
|
|
356
357
|
There is no dead code to golf away (checked); shrinking further means
|
|
357
358
|
deleting either a technique or an explanation.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tiny-datalog"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A Datalog engine small enough to read in an afternoon"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{name = "Andrew Goodchild"}]
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["datalog", "logic programming", "deductive database",
|
|
16
|
+
"semi-naive evaluation", "magic sets", "provenance",
|
|
17
|
+
"stable models", "defeasible logic", "teaching"]
|
|
18
|
+
# No "License ::" classifier: `license` above is an SPDX expression, and
|
|
19
|
+
# setuptools refuses a build that declares the licence both ways.
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Intended Audience :: Education",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Database",
|
|
34
|
+
"Topic :: Education",
|
|
35
|
+
"Topic :: Software Development :: Interpreters",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/andrewgoodchild/tiny-datalog"
|
|
41
|
+
Source = "https://github.com/andrewgoodchild/tiny-datalog"
|
|
42
|
+
Issues = "https://github.com/andrewgoodchild/tiny-datalog/issues"
|
|
43
|
+
Changelog = "https://github.com/andrewgoodchild/tiny-datalog/releases"
|
|
44
|
+
Lessons = "https://github.com/andrewgoodchild/tiny-datalog/tree/main/lessons"
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
tiny-datalog = "tiny_datalog.datalog:main"
|
|
48
|
+
tiny-datalog-prolog = "tiny_datalog.prolog:main"
|
|
49
|
+
tiny-datalog-semiring = "tiny_datalog.semiring:main"
|
|
50
|
+
tiny-datalog-tabling = "tiny_datalog.tabling:main"
|
|
51
|
+
tiny-datalog-incremental = "tiny_datalog.incremental:main"
|
|
52
|
+
tiny-datalog-subsumption = "tiny_datalog.subsumption:main"
|
|
53
|
+
tiny-datalog-containment = "tiny_datalog.containment:main"
|
|
54
|
+
tiny-datalog-defeasible = "tiny_datalog.defeasible:main"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
include = ["tiny_datalog*"]
|
|
@@ -10,7 +10,8 @@ Everything else in the package is a satellite built on top of it:
|
|
|
10
10
|
models), `semiring` (provenance-weighted evaluation), `incremental`
|
|
11
11
|
(maintenance under updates), `tabling` (top-down with memoing),
|
|
12
12
|
`subsumption` (KL-ONE style classification), `containment` (query
|
|
13
|
-
containment),
|
|
13
|
+
containment), `defeasible` (rules with exceptions and priorities), and
|
|
14
|
+
`prolog` (a Prolog reader for the same syntax).
|
|
14
15
|
Import those by name: `from tiny_datalog import semiring`.
|
|
15
16
|
"""
|
|
16
17
|
|
|
@@ -29,7 +30,7 @@ from tiny_datalog.datalog import (
|
|
|
29
30
|
format_atom, format_fact,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
|
-
__version__ = "0.
|
|
33
|
+
__version__ = "0.2.0"
|
|
33
34
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
"Var", "Const", "Struct", "Atom", "Literal", "Rule",
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
defeasible.py — defeasible logic: rules with exceptions and priorities.
|
|
4
|
+
(Lesson 18, which ends with a tour of this module.)
|
|
5
|
+
|
|
6
|
+
Lesson 3 met defaults through `not`: birds fly unless abnormal. That
|
|
7
|
+
works for one exception, written by hand into the rule it overrides.
|
|
8
|
+
Real rule sets — statutes, policies, eligibility criteria — are made of
|
|
9
|
+
generalisations, exceptions to them, exceptions to the exceptions, and
|
|
10
|
+
an order saying which wins. Defeasible logic (Nute, 1994) makes all of
|
|
11
|
+
that first-class:
|
|
12
|
+
|
|
13
|
+
r1: penguin(X) -> bird(X). % strict: no exceptions, ever
|
|
14
|
+
r2: bird(X) => flies(X). % defeasible: usually
|
|
15
|
+
r3: penguin(X) => ~flies(X). % a rule for the opposite
|
|
16
|
+
r4: injured(X) ~> ~flies(X). % a defeater: can only block
|
|
17
|
+
r3 > r2. % superiority: r3 beats r2
|
|
18
|
+
|
|
19
|
+
`~` is *strong* negation — `~flies(opus)` is a claim, not an absence —
|
|
20
|
+
and a conflict is exactly a pair of complementary literals. There is
|
|
21
|
+
no `not` here at all: exceptions are rules that attack, priorities say
|
|
22
|
+
who wins, and an unresolved conflict yields neither side instead of
|
|
23
|
+
whichever rule happened to have the `not` written into it.
|
|
24
|
+
|
|
25
|
+
Conclusions carry four tags (Antoniou, Billington, Governatori & Maher
|
|
26
|
+
2001):
|
|
27
|
+
|
|
28
|
+
+Δ q q is definitely provable: facts and strict rules alone
|
|
29
|
+
−Δ q q is demonstrably not definitely provable
|
|
30
|
+
+∂ q q is defeasibly provable: some applicable rule supports q,
|
|
31
|
+
~q is not definite, and every rule for ~q is either
|
|
32
|
+
inapplicable or beaten by an applicable rule for q
|
|
33
|
+
−∂ q q is demonstrably not defeasibly provable
|
|
34
|
+
|
|
35
|
+
"Demonstrably": a −tag is a finite proof of failure, not the mere
|
|
36
|
+
absence of a +tag. On a loop like `a => b. b => a.` neither +∂ b nor
|
|
37
|
+
−∂ b is ever established, and the module reports b as *undecided* —
|
|
38
|
+
where Lesson 5's well-founded semantics would settle it false, as does
|
|
39
|
+
the well-founded variant of this logic (Maher & Governatori 1999).
|
|
40
|
+
Standard defeasible logic is characterised instead by Kunen's
|
|
41
|
+
three-valued semantics of a logic program that encodes the theory —
|
|
42
|
+
which is why it cannot see that such a loop has no foundation.
|
|
43
|
+
|
|
44
|
+
The proof conditions are implemented one-to-one in `_conclude`, over
|
|
45
|
+
the theory's grounding, as a fixpoint: tags are only ever added, so
|
|
46
|
+
iteration stops. Grounding reuses the core engine. Every rule, read
|
|
47
|
+
as if strict, is a positive Datalog rule, and its least model bounds
|
|
48
|
+
everything a +tag could be about; see `Theory.ground` for the one
|
|
49
|
+
consequence of grounding that way. `--propagating` switches from
|
|
50
|
+
ambiguity blocking to ambiguity propagation (`programs/ambiguity.dfl`
|
|
51
|
+
shows the difference).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import re
|
|
56
|
+
import sys
|
|
57
|
+
from collections import defaultdict
|
|
58
|
+
from itertools import combinations
|
|
59
|
+
|
|
60
|
+
from tiny_datalog.datalog import (
|
|
61
|
+
Atom, DatalogError, Engine, Literal, ParseError, Program, Rule,
|
|
62
|
+
SafetyError, Var, format_atom, match_answers, parse_goal, read_program,
|
|
63
|
+
_sort_key)
|
|
64
|
+
|
|
65
|
+
STRICT, DEFEASIBLE, DEFEATER = "->", "=>", "~>"
|
|
66
|
+
|
|
67
|
+
_TOKEN = re.compile(r"""
|
|
68
|
+
(?P<comment>%[^\n]*) | (?P<space>\s+)
|
|
69
|
+
| (?P<string>"[^"\n]*"|'[^'\n]*')
|
|
70
|
+
| (?P<number>-?[0-9]+(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?)
|
|
71
|
+
| (?P<arrow>->|=>|~>) | (?P<punct>[:,.()>~])
|
|
72
|
+
| (?P<name>[A-Za-z_][A-Za-z0-9_]*)
|
|
73
|
+
""", re.VERBOSE)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def complement(lit):
|
|
77
|
+
"""`p` <-> `~p`. A literal is (pred, args); strong negation lives
|
|
78
|
+
in the predicate name, so the core engine stores ~p as a relation
|
|
79
|
+
of its own and never needs to know it is special."""
|
|
80
|
+
pred, args = lit
|
|
81
|
+
return (pred[1:] if pred.startswith("~") else "~" + pred), args
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def show(lit):
|
|
85
|
+
return format_atom(*lit)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Theory:
|
|
89
|
+
"""A parsed defeasible theory: facts, labelled rules, superiority."""
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.facts = [] # core Atoms; a ~ fact has pred "~p"
|
|
93
|
+
self.rules = [] # (label, kind, head Atom, body Atoms)
|
|
94
|
+
self.superior = set() # (stronger label, weaker label)
|
|
95
|
+
|
|
96
|
+
# -- reading ------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def parse(cls, text):
|
|
100
|
+
theory = cls()
|
|
101
|
+
tokens = _tokens(text)
|
|
102
|
+
statement = []
|
|
103
|
+
for tok in tokens:
|
|
104
|
+
statement.append(tok)
|
|
105
|
+
if tok[1] == "." and _depth(statement) == 0:
|
|
106
|
+
theory._statement(statement[:-1], tok[2])
|
|
107
|
+
statement = []
|
|
108
|
+
if statement:
|
|
109
|
+
raise ParseError("line %d: expected '.', got end of input"
|
|
110
|
+
% statement[-1][2])
|
|
111
|
+
theory._check()
|
|
112
|
+
return theory
|
|
113
|
+
|
|
114
|
+
def _statement(self, toks, line):
|
|
115
|
+
text = [t[1] for t in toks]
|
|
116
|
+
if len(toks) == 3 and text[1] == ">": # r3 > r2.
|
|
117
|
+
self.superior.add((text[0], text[2]))
|
|
118
|
+
return
|
|
119
|
+
label = None
|
|
120
|
+
if len(toks) > 1 and toks[0][0] == "name" and text[1] == ":":
|
|
121
|
+
label, toks = text[0], toks[2:]
|
|
122
|
+
arrows = [i for i, t in enumerate(toks) if t[0] == "arrow"]
|
|
123
|
+
if not arrows:
|
|
124
|
+
if label is not None:
|
|
125
|
+
raise ParseError("line %d: a labelled statement must be a "
|
|
126
|
+
"rule (->, => or ~>)" % line)
|
|
127
|
+
self.facts.append(_atom(toks, line))
|
|
128
|
+
return
|
|
129
|
+
if len(arrows) > 1:
|
|
130
|
+
raise ParseError("line %d: one arrow per rule" % line)
|
|
131
|
+
i = arrows[0]
|
|
132
|
+
body = [_atom(part, line) for part in _split(toks[:i])]
|
|
133
|
+
head = _atom(toks[i + 1:], line)
|
|
134
|
+
self.rules.append((label or "_r%d" % (len(self.rules) + 1),
|
|
135
|
+
toks[i][1], head, tuple(_fresh_anonymous(body))))
|
|
136
|
+
|
|
137
|
+
def _check(self):
|
|
138
|
+
labels = [r[0] for r in self.rules]
|
|
139
|
+
dup = {l for l in labels if labels.count(l) > 1}
|
|
140
|
+
if dup:
|
|
141
|
+
raise ParseError("rule label used twice: %s" % ", ".join(sorted(dup)))
|
|
142
|
+
for pair in self.superior:
|
|
143
|
+
for l in pair:
|
|
144
|
+
if l not in labels:
|
|
145
|
+
raise ParseError("superiority names unknown rule %s" % l)
|
|
146
|
+
# the superiority relation must be acyclic, or "r beats s"
|
|
147
|
+
# could end up meaning r beats itself
|
|
148
|
+
beats = defaultdict(set)
|
|
149
|
+
for a, b in self.superior:
|
|
150
|
+
beats[a].add(b)
|
|
151
|
+
for start in list(beats):
|
|
152
|
+
stack, seen = list(beats[start]), set()
|
|
153
|
+
while stack:
|
|
154
|
+
x = stack.pop()
|
|
155
|
+
if x == start:
|
|
156
|
+
raise SafetyError("superiority is cyclic through %s"
|
|
157
|
+
% start)
|
|
158
|
+
if x not in seen:
|
|
159
|
+
seen.add(x)
|
|
160
|
+
stack.extend(beats[x])
|
|
161
|
+
|
|
162
|
+
# -- grounding ----------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def ground(self):
|
|
165
|
+
"""Ground rules (label, kind, head, body) over literals, and the
|
|
166
|
+
set of facts.
|
|
167
|
+
|
|
168
|
+
Every rule is first read as a strict positive Datalog rule; the
|
|
169
|
+
core engine's least model of that program — the *envelope* —
|
|
170
|
+
holds every literal any rule could ever establish. A rule
|
|
171
|
+
instance is kept when its variables can all be bound by body
|
|
172
|
+
literals inside the envelope; the body literals it does not
|
|
173
|
+
match there simply fail. So `p, q => r` with q underivable is
|
|
174
|
+
kept, and r is refuted (−∂) as the proof theory says, while an
|
|
175
|
+
instance no derivable literal could bind at all is never made.
|
|
176
|
+
|
|
177
|
+
That is the one place this departs from the proof theory, which
|
|
178
|
+
ranges over every constant. A loop nothing starts, written with
|
|
179
|
+
variables — `a(X) => b(X). b(X) => a(X).` — is not reported at
|
|
180
|
+
all, where the proof theory would leave each instance undecided.
|
|
181
|
+
Written without variables it needs no binding, is kept, and
|
|
182
|
+
comes out undecided (`programs/loops.dfl`)."""
|
|
183
|
+
rules = [Rule(head, tuple(Literal(b, False) for b in body))
|
|
184
|
+
for _l, _k, head, body in self.rules]
|
|
185
|
+
program = Program([Rule(a, ()) for a in self.facts] + rules)
|
|
186
|
+
arities = defaultdict(set)
|
|
187
|
+
for pred, n in program.arity.items():
|
|
188
|
+
arities[pred.lstrip("~")].add(n)
|
|
189
|
+
for pred, ns in sorted(arities.items()):
|
|
190
|
+
if len(ns) > 1:
|
|
191
|
+
raise SafetyError("predicate %s used with arities %s (its "
|
|
192
|
+
"negation counts too)"
|
|
193
|
+
% (pred, " and ".join(map(str, sorted(ns)))))
|
|
194
|
+
engine = Engine(program)
|
|
195
|
+
engine.run()
|
|
196
|
+
facts = {(a.pred, tuple(x.value for x in a.args)) for a in self.facts}
|
|
197
|
+
ground, seen = [], set()
|
|
198
|
+
for label, kind, head, body in self.rules:
|
|
199
|
+
needed = _variables(body)
|
|
200
|
+
# match every subset of the body that binds all variables
|
|
201
|
+
for n in range(len(body) + 1):
|
|
202
|
+
for part in combinations(body, n):
|
|
203
|
+
if _variables(part) != needed:
|
|
204
|
+
continue
|
|
205
|
+
sub = Rule(head, tuple(Literal(b, False) for b in part))
|
|
206
|
+
for subst in engine._rule_substitutions(sub):
|
|
207
|
+
g = (label, kind,
|
|
208
|
+
(head.pred, engine._instantiate(head, subst)),
|
|
209
|
+
tuple((b.pred, engine._instantiate(b, subst))
|
|
210
|
+
for b in body))
|
|
211
|
+
if g not in seen:
|
|
212
|
+
seen.add(g)
|
|
213
|
+
ground.append(g)
|
|
214
|
+
return facts, ground
|
|
215
|
+
|
|
216
|
+
# -- the proof theory ---------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def conclusions(self, policy="blocking"):
|
|
219
|
+
"""{tag: set of literals} for the tags +Δ, −Δ, +∂, −∂, and
|
|
220
|
+
'undecided' for literals that got neither +∂ nor −∂."""
|
|
221
|
+
if policy not in ("blocking", "propagating"):
|
|
222
|
+
raise DatalogError("unknown policy %r: use 'blocking' or "
|
|
223
|
+
"'propagating'" % (policy,))
|
|
224
|
+
facts, ground = self.ground()
|
|
225
|
+
return _conclude(facts, ground, self.superior, policy)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _tokens(text):
|
|
229
|
+
"""(kind, text, line) triples; comments and whitespace dropped."""
|
|
230
|
+
tokens, pos = [], 0
|
|
231
|
+
while pos < len(text):
|
|
232
|
+
m = _TOKEN.match(text, pos)
|
|
233
|
+
if m is None:
|
|
234
|
+
raise ParseError("line %d: unexpected character %r"
|
|
235
|
+
% (text.count("\n", 0, pos) + 1, text[pos]))
|
|
236
|
+
if m.lastgroup not in ("space", "comment"):
|
|
237
|
+
tokens.append((m.lastgroup, m.group(),
|
|
238
|
+
text.count("\n", 0, pos) + 1))
|
|
239
|
+
pos = m.end()
|
|
240
|
+
return tokens
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _variables(atoms):
|
|
244
|
+
return {a.name for atom in atoms for a in atom.args if isinstance(a, Var)}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _depth(toks):
|
|
248
|
+
return sum({"(": 1, ")": -1}.get(t[1], 0) for t in toks)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _split(toks):
|
|
252
|
+
"""Comma-separated parts at bracket depth 0."""
|
|
253
|
+
parts, cur, depth = [], [], 0
|
|
254
|
+
for t in toks:
|
|
255
|
+
depth += {"(": 1, ")": -1}.get(t[1], 0)
|
|
256
|
+
if t[1] == "," and depth == 0:
|
|
257
|
+
parts.append(cur)
|
|
258
|
+
cur = []
|
|
259
|
+
else:
|
|
260
|
+
cur.append(t)
|
|
261
|
+
return parts + [cur] if cur else parts
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _atom(toks, line):
|
|
265
|
+
"""`[~] atom` -> core Atom, pred prefixed '~' when negated. The atom
|
|
266
|
+
itself goes through the core parser, so constants, strings,
|
|
267
|
+
variables and its error messages are the ones the course knows."""
|
|
268
|
+
if not toks:
|
|
269
|
+
raise ParseError("line %d: expected a literal" % line)
|
|
270
|
+
neg = toks[0][1] == "~"
|
|
271
|
+
toks = toks[1:] if neg else toks
|
|
272
|
+
if not toks or toks[0][0] != "name" or toks[0][1][0].isupper():
|
|
273
|
+
raise ParseError("line %d: expected a literal, got %r"
|
|
274
|
+
% (line, " ".join(t[1] for t in toks) or "nothing"))
|
|
275
|
+
atom = parse_goal(" ".join(t[1] for t in toks))
|
|
276
|
+
return Atom(("~" if neg else "") + atom.pred, atom.args)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _fresh_anonymous(atoms):
|
|
280
|
+
"""Each literal was parsed on its own, so each numbered its `_`s from
|
|
281
|
+
1; renumber so two `_` in one rule stay two different variables."""
|
|
282
|
+
n = 0
|
|
283
|
+
out = []
|
|
284
|
+
for a in atoms:
|
|
285
|
+
args = []
|
|
286
|
+
for x in a.args:
|
|
287
|
+
if isinstance(x, Var) and x.anonymous:
|
|
288
|
+
n += 1
|
|
289
|
+
x = Var("_#%d" % n)
|
|
290
|
+
args.append(x)
|
|
291
|
+
out.append(Atom(a.pred, tuple(args)))
|
|
292
|
+
return out
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _conclude(facts, ground, superior, policy):
|
|
296
|
+
"""The proof conditions, as a fixpoint (Antoniou, Billington,
|
|
297
|
+
Governatori & Maher 2001). For a literal q, R[q] is every rule for
|
|
298
|
+
q, Rs[q] the strict ones, Rsd[q] strict or defeasible — defeaters
|
|
299
|
+
may attack, never support — and A(r) is rule r's body:
|
|
300
|
+
|
|
301
|
+
+Δq q is a fact, or some r in Rs[q] has A(r) all +Δ.
|
|
302
|
+
−Δq q is not a fact, and every r in Rs[q] has some a in A(r) −Δ.
|
|
303
|
+
+∂q +Δq; or (1) some r in Rsd[q] has A(r) all +∂, (2) ~q is −Δ,
|
|
304
|
+
and (3) every attacker s in R[~q] is either dead — some a in
|
|
305
|
+
A(s) is −∂ — or beaten: some t in Rsd[q] with A(t) all +∂
|
|
306
|
+
and t > s. The t may differ per attacker: team defeat.
|
|
307
|
+
−∂q −Δq, and: every r in Rsd[q] has some a in A(r) −∂; or ~q is
|
|
308
|
+
+Δ; or some attacker s in R[~q] is live — A(s) all +∂ — and
|
|
309
|
+
every t in Rsd[q] has a body literal −∂ or is not above s.
|
|
310
|
+
|
|
311
|
+
That is ambiguity *blocking*: an attacker only counts once its body
|
|
312
|
+
is proved. Under ambiguity *propagation* (Maher 2012) the same
|
|
313
|
+
conditions hold with attackers judged by mere *support*, σ:
|
|
314
|
+
|
|
315
|
+
+σq +Δq, or some r in Rsd[q] has A(r) all +σ, and no attacker
|
|
316
|
+
s in R[~q] with no −∂ body literal is above r.
|
|
317
|
+
−σq −Δq, and every r in Rsd[q] has a body literal −σ, or is
|
|
318
|
+
below some attacker s in R[~q] with A(s) all +∂.
|
|
319
|
+
|
|
320
|
+
and in +∂ an attacker is dead only if a body literal is −σ, while in
|
|
321
|
+
−∂ it is live as soon as A(s) is all +σ. Supported-but-unproved
|
|
322
|
+
attackers then still block, so doubt spreads downstream."""
|
|
323
|
+
by_head = defaultdict(list)
|
|
324
|
+
for rule in ground:
|
|
325
|
+
by_head[rule[2]].append(rule)
|
|
326
|
+
# report on every literal the theory mentions; decide its complement
|
|
327
|
+
# too, since every +∂ / −∂ condition looks at ~q
|
|
328
|
+
mentioned = set(facts) | set(by_head) | {a for r in ground for a in r[3]}
|
|
329
|
+
universe = mentioned | {complement(q) for q in mentioned}
|
|
330
|
+
|
|
331
|
+
def strict(q):
|
|
332
|
+
return [r for r in by_head[q] if r[1] == STRICT]
|
|
333
|
+
|
|
334
|
+
def supporting(q):
|
|
335
|
+
return [r for r in by_head[q] if r[1] != DEFEATER]
|
|
336
|
+
|
|
337
|
+
def body_in(r, tagged):
|
|
338
|
+
return all(a in tagged for a in r[3])
|
|
339
|
+
|
|
340
|
+
def body_hits(r, tagged):
|
|
341
|
+
return any(a in tagged for a in r[3])
|
|
342
|
+
|
|
343
|
+
def above(t, s):
|
|
344
|
+
return (t[0], s[0]) in superior
|
|
345
|
+
|
|
346
|
+
plus_d, minus_d = set(), set() # +Δ, −Δ: the strict part first
|
|
347
|
+
changed = True
|
|
348
|
+
while changed:
|
|
349
|
+
changed = False
|
|
350
|
+
for q in universe:
|
|
351
|
+
if q not in plus_d and (q in facts or any(
|
|
352
|
+
body_in(r, plus_d) for r in strict(q))):
|
|
353
|
+
plus_d.add(q)
|
|
354
|
+
changed = True
|
|
355
|
+
if q not in minus_d and q not in facts and all(
|
|
356
|
+
body_hits(r, minus_d) for r in strict(q)):
|
|
357
|
+
minus_d.add(q)
|
|
358
|
+
changed = True
|
|
359
|
+
|
|
360
|
+
plus, minus = set(), set() # +∂, −∂
|
|
361
|
+
if policy == "propagating":
|
|
362
|
+
s_plus, s_minus = set(), set() # +σ, −σ: who is merely supported
|
|
363
|
+
else:
|
|
364
|
+
s_plus, s_minus = plus, minus # blocking: support = proof
|
|
365
|
+
changed = True
|
|
366
|
+
while changed:
|
|
367
|
+
changed = False
|
|
368
|
+
for q in universe:
|
|
369
|
+
nq = complement(q)
|
|
370
|
+
attackers = by_head[nq]
|
|
371
|
+
if q not in plus and (q in plus_d or (
|
|
372
|
+
any(body_in(r, plus) for r in supporting(q))
|
|
373
|
+
and nq in minus_d
|
|
374
|
+
and all(body_hits(s, s_minus) or any(
|
|
375
|
+
body_in(t, plus) and above(t, s)
|
|
376
|
+
for t in supporting(q)) for s in attackers))):
|
|
377
|
+
plus.add(q)
|
|
378
|
+
changed = True
|
|
379
|
+
if q not in minus and q in minus_d and (
|
|
380
|
+
all(body_hits(r, minus) for r in supporting(q))
|
|
381
|
+
or nq in plus_d
|
|
382
|
+
or any(body_in(s, s_plus) and all(
|
|
383
|
+
body_hits(t, minus) or not above(t, s)
|
|
384
|
+
for t in supporting(q)) for s in attackers)):
|
|
385
|
+
minus.add(q)
|
|
386
|
+
changed = True
|
|
387
|
+
if policy != "propagating":
|
|
388
|
+
continue
|
|
389
|
+
if q not in s_plus and (q in plus_d or any(
|
|
390
|
+
body_in(r, s_plus) and all(
|
|
391
|
+
body_hits(s, minus) or not above(s, r)
|
|
392
|
+
for s in attackers)
|
|
393
|
+
for r in supporting(q))):
|
|
394
|
+
s_plus.add(q)
|
|
395
|
+
changed = True
|
|
396
|
+
if q not in s_minus and q in minus_d and all(
|
|
397
|
+
body_hits(r, s_minus) or any(
|
|
398
|
+
body_in(s, plus) and above(s, r) for s in attackers)
|
|
399
|
+
for r in supporting(q)):
|
|
400
|
+
s_minus.add(q)
|
|
401
|
+
changed = True
|
|
402
|
+
return {"+Δ": plus_d & mentioned, "−Δ": minus_d & mentioned,
|
|
403
|
+
"+∂": plus & mentioned, "−∂": minus & mentioned,
|
|
404
|
+
"undecided": mentioned - plus - minus}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def load(text):
|
|
408
|
+
return Theory.parse(text)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
TAG_NAMES = [("+Δ", "definitely"), ("+∂", "defeasibly"),
|
|
412
|
+
("−∂", "not defeasibly"), ("undecided", "undecided")]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def main(argv=None):
|
|
416
|
+
ap = argparse.ArgumentParser(
|
|
417
|
+
description="Defeasible logic: strict rules (->), defeasible rules "
|
|
418
|
+
"(=>), defeaters (~>), superiority (r1 > r2).")
|
|
419
|
+
ap.add_argument("file", help="defeasible theory")
|
|
420
|
+
ap.add_argument("-q", "--query", action="append", default=[],
|
|
421
|
+
metavar="LITERAL",
|
|
422
|
+
help="show the tags of matching literals (repeatable), "
|
|
423
|
+
"e.g. -q 'flies(X)' or -q '~flies(X)'")
|
|
424
|
+
ap.add_argument("--propagating", action="store_true",
|
|
425
|
+
help="ambiguity propagating instead of blocking")
|
|
426
|
+
args = ap.parse_args(argv)
|
|
427
|
+
try:
|
|
428
|
+
theory = load(read_program(args.file))
|
|
429
|
+
result = theory.conclusions(
|
|
430
|
+
"propagating" if args.propagating else "blocking")
|
|
431
|
+
queries = [_atom(_tokens(q.rstrip(". ")), 1) for q in args.query]
|
|
432
|
+
except DatalogError as exc:
|
|
433
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
434
|
+
return 1
|
|
435
|
+
|
|
436
|
+
def ordered(lits):
|
|
437
|
+
return sorted(lits, key=lambda l: (l[0].lstrip("~"), l[0],
|
|
438
|
+
_sort_key(l[1])))
|
|
439
|
+
|
|
440
|
+
if queries:
|
|
441
|
+
for q in queries:
|
|
442
|
+
print("?- %s" % (q.pred if not q.args else "%s(%s)" % (
|
|
443
|
+
q.pred, ", ".join(str(a) for a in q.args))))
|
|
444
|
+
hits = [l for l in ordered(set().union(*result.values()))
|
|
445
|
+
if l[0] == q.pred and match_answers(q, [l[1]])]
|
|
446
|
+
for lit in hits:
|
|
447
|
+
tags = [t for t, _n in TAG_NAMES if lit in result[t]]
|
|
448
|
+
print(" %-28s %s" % (show(lit), " ".join(tags)))
|
|
449
|
+
if not hits:
|
|
450
|
+
print(" (no rule or fact mentions it)")
|
|
451
|
+
return 0
|
|
452
|
+
for tag, name in TAG_NAMES:
|
|
453
|
+
lits = ordered(result[tag])
|
|
454
|
+
print("%s %s (%d)" % (tag, name, len(lits)) if tag != "undecided"
|
|
455
|
+
else "%s (%d)" % (name, len(lits)))
|
|
456
|
+
for lit in lits:
|
|
457
|
+
print(" " + show(lit))
|
|
458
|
+
return 0
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
if __name__ == "__main__":
|
|
462
|
+
sys.exit(main())
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tiny-datalog
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Datalog engine small enough to read in an afternoon
|
|
5
|
+
Author: Andrew Goodchild
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/andrewgoodchild/tiny-datalog
|
|
8
|
+
Project-URL: Source, https://github.com/andrewgoodchild/tiny-datalog
|
|
9
|
+
Project-URL: Issues, https://github.com/andrewgoodchild/tiny-datalog/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/andrewgoodchild/tiny-datalog/releases
|
|
11
|
+
Project-URL: Lessons, https://github.com/andrewgoodchild/tiny-datalog/tree/main/lessons
|
|
12
|
+
Keywords: datalog,logic programming,deductive database,semi-naive evaluation,magic sets,provenance,stable models,defeasible logic,teaching
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Database
|
|
26
|
+
Classifier: Topic :: Education
|
|
27
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
1
34
|
# tiny-datalog
|
|
2
35
|
|
|
3
36
|
[](https://github.com/andrewgoodchild/tiny-datalog/actions/workflows/ci.yml)
|
|
@@ -93,7 +126,7 @@ Nothing to install:
|
|
|
93
126
|
|
|
94
127
|
```sh
|
|
95
128
|
git clone https://github.com/andrewgoodchild/tiny-datalog && cd tiny-datalog
|
|
96
|
-
python3 tests.py #
|
|
129
|
+
python3 tests.py # 246 tests, ~12s
|
|
97
130
|
```
|
|
98
131
|
|
|
99
132
|
## Why the language choice decides what you can ask later
|
|
@@ -198,6 +231,15 @@ tabled evaluation all agree, and that incremental maintenance matches
|
|
|
198
231
|
recomputation under random updates. 400 programs per run;
|
|
199
232
|
`TINY_DATALOG_FUZZ=3000 python3 tests.py DifferentialFuzzTests` soaks.
|
|
200
233
|
|
|
234
|
+
Answers are also checked against engines this repository did not write.
|
|
235
|
+
[`conformance/`](https://github.com/andrewgoodchild/tiny-datalog/tree/main/conformance)
|
|
236
|
+
runs the [datalog-conformance](https://pypi.org/project/datalog-conformance/)
|
|
237
|
+
corpus, harvested from Soufflé, Nemo and Crepe, through all four
|
|
238
|
+
strategies, and its defeasible theories, from SPINdle and the papers,
|
|
239
|
+
through `defeasible.py`: 195 cases pass. Every skip is named with its
|
|
240
|
+
reason: arithmetic and 10⁵-fact joins, which this course omits on
|
|
241
|
+
purpose, or cases written for a different logic.
|
|
242
|
+
|
|
201
243
|
## Learning Datalog
|
|
202
244
|
|
|
203
245
|
`lessons/` is a complete course, no prior exposure assumed, every
|
|
@@ -206,7 +248,8 @@ the current research threads. The field's own recent lecture notes
|
|
|
206
248
|
observe that the literature advises people building Datalog engines
|
|
207
249
|
better than people trying to *use* one; this course does both halves
|
|
208
250
|
on purpose — sixteen lessons where the engine is the explanation, then
|
|
209
|
-
a lesson on authoring rules that survive review
|
|
251
|
+
a lesson on authoring rules that survive review, and one on a logic
|
|
252
|
+
built for rules with exceptions. And it is built to be
|
|
210
253
|
inherited: `git clone`, no dependencies, no hosted anything, and every
|
|
211
254
|
quoted transcript re-verified by CI — the exercises cannot rot. (For where each
|
|
212
255
|
technique ships — CodeQL, RDFox, Feldera, SNOMED and the rest —
|
|
@@ -287,7 +330,7 @@ for service, cve in sorted(engine.rels["exposed"]):
|
|
|
287
330
|
|
|
288
331
|
The command-line interface installs too, as `tiny-datalog` (and
|
|
289
332
|
`tiny-datalog-semiring`, `-tabling`, `-incremental`, `-subsumption`,
|
|
290
|
-
`-containment`, `-prolog` for the satellites):
|
|
333
|
+
`-containment`, `-defeasible`, `-prolog` for the satellites):
|
|
291
334
|
|
|
292
335
|
```sh
|
|
293
336
|
tiny-datalog -q 'exposed(S, C)' supply-chain.dl
|
|
@@ -311,15 +354,18 @@ tiny_datalog/ the engine and its satellites — the code you read:
|
|
|
311
354
|
tabling.py tabled top-down evaluation (iterative QSQR)
|
|
312
355
|
subsumption.py KL-ONE-style EL classifier, compiled to Datalog
|
|
313
356
|
containment.py query containment and minimisation by homomorphism
|
|
357
|
+
defeasible.py defeasible logic: exceptions, priorities, defeaters
|
|
314
358
|
*.py three-line launchers, so `python3 datalog.py ...` works
|
|
315
359
|
straight from a checkout with nothing installed
|
|
316
360
|
programs/ teaching programs, numbered by the lesson that uses
|
|
317
361
|
them (00-* are the README's examples)
|
|
318
|
-
lessons/ getting started, glossary, and lessons 0–
|
|
362
|
+
lessons/ getting started, glossary, and lessons 0–19
|
|
319
363
|
exercises/ worked answers, verified by the test suite
|
|
320
364
|
cases/ golden test cases — add one without writing Python
|
|
365
|
+
conformance/ the external datalog-conformance corpus (Soufflé, Nemo,
|
|
366
|
+
Crepe, SPINdle), run against every evaluation strategy
|
|
321
367
|
benchmarks/ scaled input generators (chain/tree/clique/grid)
|
|
322
|
-
tests.py
|
|
368
|
+
tests.py 246 tests: every shipped program and exercise answer is
|
|
323
369
|
executed, a conformance suite runs every query through
|
|
324
370
|
every applicable strategy, and a seeded fuzzer checks
|
|
325
371
|
the same property on random programs
|
|
@@ -333,13 +379,13 @@ used.
|
|
|
333
379
|
### How big is it, honestly
|
|
334
380
|
|
|
335
381
|
The evaluator is about 850 lines (`tiny_datalog/datalog.py`, up to the
|
|
336
|
-
command-line interface), the CLI, `--explain` and why-not another 600, and the
|
|
337
|
-
2,
|
|
382
|
+
command-line interface), the CLI, `--explain` and why-not another 600, and the nine satellite modules about
|
|
383
|
+
2,900. Call it 4.3k lines of toolkit and 2.7k of tests, roughly a
|
|
338
384
|
quarter of it commentary.
|
|
339
385
|
|
|
340
386
|
"Tiny" is a claim about the evaluator, and about each satellite module
|
|
341
|
-
singly: none of the
|
|
342
|
-
the repository, which is
|
|
387
|
+
singly: none of the nine exceeds 500 lines, which a test asserts. It is not a claim about
|
|
388
|
+
the repository, which is ten modules because it teaches ten things.
|
|
343
389
|
|
|
344
390
|
There is no dead code to golf away (checked); shrinking further means
|
|
345
391
|
deleting either a technique or an explanation.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[console_scripts]
|
|
2
2
|
tiny-datalog = tiny_datalog.datalog:main
|
|
3
3
|
tiny-datalog-containment = tiny_datalog.containment:main
|
|
4
|
+
tiny-datalog-defeasible = tiny_datalog.defeasible:main
|
|
4
5
|
tiny-datalog-incremental = tiny_datalog.incremental:main
|
|
5
6
|
tiny-datalog-prolog = tiny_datalog.prolog:main
|
|
6
7
|
tiny-datalog-semiring = tiny_datalog.semiring:main
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=77"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "tiny-datalog"
|
|
7
|
-
version = "0.1.1"
|
|
8
|
-
description = "A Datalog engine small enough to read in an afternoon"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
license = "MIT"
|
|
11
|
-
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.9"
|
|
13
|
-
authors = [{name = "Andrew Goodchild"}]
|
|
14
|
-
dependencies = []
|
|
15
|
-
|
|
16
|
-
[project.urls]
|
|
17
|
-
Homepage = "https://github.com/andrewgoodchild/tiny-datalog"
|
|
18
|
-
|
|
19
|
-
[project.scripts]
|
|
20
|
-
tiny-datalog = "tiny_datalog.datalog:main"
|
|
21
|
-
tiny-datalog-prolog = "tiny_datalog.prolog:main"
|
|
22
|
-
tiny-datalog-semiring = "tiny_datalog.semiring:main"
|
|
23
|
-
tiny-datalog-tabling = "tiny_datalog.tabling:main"
|
|
24
|
-
tiny-datalog-incremental = "tiny_datalog.incremental:main"
|
|
25
|
-
tiny-datalog-subsumption = "tiny_datalog.subsumption:main"
|
|
26
|
-
tiny-datalog-containment = "tiny_datalog.containment:main"
|
|
27
|
-
|
|
28
|
-
[tool.setuptools.packages.find]
|
|
29
|
-
include = ["tiny_datalog*"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|