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.
Files changed (22) hide show
  1. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/PKG-INFO +44 -10
  2. tiny_datalog-0.1.1/tiny_datalog.egg-info/PKG-INFO → tiny_datalog-0.2.0/README.md +22 -21
  3. tiny_datalog-0.2.0/pyproject.toml +57 -0
  4. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/__init__.py +3 -2
  5. tiny_datalog-0.2.0/tiny_datalog/defeasible.py +462 -0
  6. tiny_datalog-0.1.1/README.md → tiny_datalog-0.2.0/tiny_datalog.egg-info/PKG-INFO +55 -9
  7. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/SOURCES.txt +1 -0
  8. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/entry_points.txt +1 -0
  9. tiny_datalog-0.1.1/pyproject.toml +0 -29
  10. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/LICENSE +0 -0
  11. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/setup.cfg +0 -0
  12. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/containment.py +0 -0
  13. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/datalog.py +0 -0
  14. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/incremental.py +0 -0
  15. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/magic.py +0 -0
  16. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/prolog.py +0 -0
  17. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/semantics.py +0 -0
  18. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/semiring.py +0 -0
  19. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/subsumption.py +0 -0
  20. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog/tabling.py +0 -0
  21. {tiny_datalog-0.1.1 → tiny_datalog-0.2.0}/tiny_datalog.egg-info/dependency_links.txt +0 -0
  22. {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.1.1
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 # 235 tests, ~12s
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. And it is built to be
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–18
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 235 tests: every shipped program and exercise answer is
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 eight satellite modules about
349
- 2,400. Call it 3.9k lines of toolkit and 2.6k of tests, roughly a
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 eight exceeds 500 lines, which a test asserts. It is not a claim about
354
- the repository, which is nine modules because it teaches nine things.
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
  [![tests](https://github.com/andrewgoodchild/tiny-datalog/actions/workflows/ci.yml/badge.svg)](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 # 235 tests, ~12s
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. And it is built to be
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–18
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 235 tests: every shipped program and exercise answer is
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 eight satellite modules about
349
- 2,400. Call it 3.9k lines of toolkit and 2.6k of tests, roughly a
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 eight exceeds 500 lines, which a test asserts. It is not a claim about
354
- the repository, which is nine modules because it teaches nine things.
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), and `prolog` (a Prolog reader for the same syntax).
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.1.1"
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
  [![tests](https://github.com/andrewgoodchild/tiny-datalog/actions/workflows/ci.yml/badge.svg)](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 # 235 tests, ~12s
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. And it is built to be
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–18
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 235 tests: every shipped program and exercise answer is
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 eight satellite modules about
337
- 2,400. Call it 3.9k lines of toolkit and 2.6k of tests, roughly a
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 eight exceeds 500 lines, which a test asserts. It is not a claim about
342
- the repository, which is nine modules because it teaches nine things.
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.
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  tiny_datalog/__init__.py
5
5
  tiny_datalog/containment.py
6
6
  tiny_datalog/datalog.py
7
+ tiny_datalog/defeasible.py
7
8
  tiny_datalog/incremental.py
8
9
  tiny_datalog/magic.py
9
10
  tiny_datalog/prolog.py
@@ -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