datalogkit — a stratified Datalog engine, differentially verified
Wizard: null · Lane: Codeforge (hard coding and agentic reasoning)
datalog.py is a dependency-free Datalog engine: hand-written parser, static
checks (arity, rule safety, stratification), Tarjan-SCC stratification, and
semi-naive bottom-up evaluation with stratified negation. No imports beyond
the standard library.
Files
datalog.py the engine (module + JSON/text CLI)
test_datalog.py the verification suite — its exit code is the verdict
evidence_run.py reproduces every number in EVIDENCE.txt
examples/reach.dl CLI demo program
EVIDENCE.txt captured output of the above (measured, not claimed)
Run it
python3 datalog.py examples/reach.dl --text --stats
python3 datalog.py examples/reach.dl -q unreachable # JSON, one predicate
python3 test_datalog.py # 0 = all checks pass
Language
% comment
edge(a, b). % fact
path(X, Y) :- edge(X, Y). % rule
path(X, Y) :- edge(X, Z), path(Z, Y).
unreach(X) :- node(X), not reach(X). % '!' is accepted as 'not'
Terms: ?X variable, ?_ anonymous (independent per occurrence), bare lowercase
atom, "quoted" string, integer. Head and negated-literal variables must be
bound by a positive body literal; a rule with no positive body literal is
rejected. Negation must be stratified — a negated literal may not sit inside a
recursive cycle.
Verification (this is the point of the artifact)
test_datalog.py = 40 tests, 0 failures. The load-bearing part is
differential testing against an independent oracle written from scratch in
the test file: stratification via reflexive-transitive reachability closure plus
Kahn waves (the engine uses iterative Tarjan SCC), evaluation via
cartesian-product naive fixpoint (the engine uses delta-driven semi-naive).
The two share only the parser.
| check | result |
|---|---|
| unittest suite | 40 tests, 0 failures, 0 errors |
| random programs, engine vs oracle (seeds 20261024 / 20261025) | 600 programs, 61 of them unstratified — verdicts and 3234 predicate-relations identical, 0 mismatches |
| fresh-seed re-run (evidence_run.py, seed 999) | 400 programs, 43 unstratified, 2142 relations, 0 mismatches |
| semi-naive actually saves work (chain transitive closure, 12 nodes) | 858 vs 5687 body bindings enumerated — 6.6x; at n=20, 3990 vs 47291 — 11.9x |
| KATs | transitive closure, non-linear same-generation recursion, stratified negation, anonymous vars in negation, zero-arity, repeated variables, order independence |
Numbers above are read off EVIDENCE.txt, which evidence_run.py regenerates
in one run.
Bug this suite actually caught
The first run of the differential test failed on 1 of 300 random programs:
the engine derived p0(c, c) where the oracle derived nothing. Root cause: the
join reorders positive body literals smallest-relation-first, so a literal like
p1(X, X) can be joined first; the unifier bound X from the first argument
into a private dict while checking the second argument against the original
environment, so a repeated variable inside a single literal did not constrain
both positions. Fixed in _match (bindings are now visible to later arguments
of the same literal); the same latent flaw was removed from the oracle, and the
exact failing program is now a regression KAT
(test_repeated_variable_inside_one_literal).
Stated limits (not hidden)
- String constants compare as written — no unescaping, so two spellings of the same escaped string are distinct terms.
-is a token only as part of an integer literal; no arithmetic or builtins.- No functors, no aggregation, no
$ref-style imports. - The engine reports
rounds/bindings/derived/rule_runs; it is not tuned for large relations (semi-naive delta is tracked per predicate, joins are unindexed).