datalog: db + naive eval + safety analysis (Phase 3, 87/87)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s

db.sx: facts indexed by relation name, rules list, dl-add-fact!
(rejects non-ground), dl-add-rule! (rejects unsafe — head vars
not in positive body). eval.sx: dl-saturate! fixpoint, dl-query
with deduped projected results. Negation and arithmetic raise
clear errors (Phase 4/7 to follow). 15 eval tests: transitive
closure, sibling, same-gen, grandparent, cyclic reach, safety.
This commit is contained in:
2026-05-07 23:41:27 +00:00
parent 8046df7ce5
commit 9bc70fd2a9
7 changed files with 663 additions and 14 deletions

206
lib/datalog/tests/eval.sx Normal file
View File

@@ -0,0 +1,206 @@
;; lib/datalog/tests/eval.sx — naive evaluation + safety analysis tests.
(define dl-et-pass 0)
(define dl-et-fail 0)
(define dl-et-failures (list))
;; Same deep-equal helper used in other suites.
(define
dl-et-deep=?
(fn
(a b)
(cond
((and (list? a) (list? b))
(and (= (len a) (len b)) (dl-et-deq-l? a b 0)))
((and (dict? a) (dict? b))
(let
((ka (keys a)) (kb (keys b)))
(and (= (len ka) (len kb)) (dl-et-deq-d? a b ka 0))))
((and (number? a) (number? b)) (= a b))
(else (equal? a b)))))
(define
dl-et-deq-l?
(fn
(a b i)
(cond
((>= i (len a)) true)
((not (dl-et-deep=? (nth a i) (nth b i))) false)
(else (dl-et-deq-l? a b (+ i 1))))))
(define
dl-et-deq-d?
(fn
(a b ka i)
(cond
((>= i (len ka)) true)
((let ((k (nth ka i))) (not (dl-et-deep=? (get a k) (get b k))))
false)
(else (dl-et-deq-d? a b ka (+ i 1))))))
;; Set-equality on lists (order-independent, uses dl-et-deep=?).
(define
dl-et-set=?
(fn
(a b)
(and (= (len a) (len b)) (dl-et-subset? a b) (dl-et-subset? b a))))
(define
dl-et-subset?
(fn
(xs ys)
(cond
((= (len xs) 0) true)
((not (dl-et-contains? ys (first xs))) false)
(else (dl-et-subset? (rest xs) ys)))))
(define
dl-et-contains?
(fn
(xs target)
(cond
((= (len xs) 0) false)
((dl-et-deep=? (first xs) target) true)
(else (dl-et-contains? (rest xs) target)))))
(define
dl-et-test!
(fn
(name got expected)
(if
(dl-et-deep=? got expected)
(set! dl-et-pass (+ dl-et-pass 1))
(do
(set! dl-et-fail (+ dl-et-fail 1))
(append!
dl-et-failures
(str name "\n expected: " expected "\n got: " got))))))
(define
dl-et-test-set!
(fn
(name got expected)
(if
(dl-et-set=? got expected)
(set! dl-et-pass (+ dl-et-pass 1))
(do
(set! dl-et-fail (+ dl-et-fail 1))
(append!
dl-et-failures
(str
name
"\n expected (set): "
expected
"\n got: "
got))))))
(define
dl-et-throws?
(fn
(thunk)
(let
((threw false))
(do (guard (e (#t (set! threw true))) (thunk)) threw))))
(define
dl-et-run-all!
(fn
()
(do
(dl-et-test-set!
"fact lookup any"
(dl-query
(dl-program "parent(tom, bob). parent(bob, ann).")
(list (quote parent) (quote X) (quote Y)))
(list {:X (quote tom) :Y (quote bob)} {:X (quote bob) :Y (quote ann)}))
(dl-et-test-set!
"fact lookup constant arg"
(dl-query
(dl-program "parent(tom, bob). parent(tom, liz). parent(bob, ann).")
(list (quote parent) (quote tom) (quote Y)))
(list {:Y (quote bob)} {:Y (quote liz)}))
(dl-et-test-set!
"no match"
(dl-query
(dl-program "parent(tom, bob).")
(list (quote parent) (quote nobody) (quote X)))
(list))
(dl-et-test-set!
"ancestor closure"
(dl-query
(dl-program
"parent(tom, bob). parent(bob, ann). parent(ann, pat).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")
(list (quote ancestor) (quote tom) (quote X)))
(list {:X (quote bob)} {:X (quote ann)} {:X (quote pat)}))
(dl-et-test-set!
"sibling"
(dl-query
(dl-program
"parent(tom, bob). parent(tom, liz). parent(jane, bob). parent(jane, liz).\n sibling(X, Y) :- parent(P, X), parent(P, Y).")
(list (quote sibling) (quote bob) (quote Y)))
(list {:Y (quote bob)} {:Y (quote liz)}))
(dl-et-test-set!
"same-generation"
(dl-query
(dl-program
"parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(liz, joe).\n person(tom). person(bob). person(liz). person(ann). person(joe).\n sg(X, X) :- person(X).\n sg(X, Y) :- parent(P1, X), sg(P1, P2), parent(P2, Y).")
(list (quote sg) (quote ann) (quote X)))
(list {:X (quote ann)} {:X (quote joe)}))
(dl-et-test!
"ancestor count"
(let
((db (dl-program "parent(a, b). parent(b, c). parent(c, d).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")))
(do (dl-saturate! db) (len (dl-relation db "ancestor"))))
6)
(dl-et-test-set!
"grandparent"
(dl-query
(dl-program
"parent(a, b). parent(b, c). parent(c, d).\n grandparent(X, Z) :- parent(X, Y), parent(Y, Z).")
(list (quote grandparent) (quote X) (quote Y)))
(list {:X (quote a) :Y (quote c)} {:X (quote b) :Y (quote d)}))
(dl-et-test!
"no recursion infinite loop"
(let
((db (dl-program "edge(1, 2). edge(2, 3). edge(3, 1).\n reach(X, Y) :- edge(X, Y).\n reach(X, Z) :- edge(X, Y), reach(Y, Z).")))
(do (dl-saturate! db) (len (dl-relation db "reach"))))
9)
(dl-et-test!
"unsafe head var"
(dl-et-throws? (fn () (dl-program "p(X, Y) :- q(X).")))
true)
(dl-et-test!
"unsafe — empty body"
(dl-et-throws? (fn () (dl-program "p(X) :- .")))
true)
(dl-et-test!
"underscore var ok"
(dl-et-throws? (fn () (dl-program "p(X, _) :- q(X).")))
false)
(dl-et-test!
"var only in head — unsafe"
(dl-et-throws? (fn () (dl-program "p(X, Y) :- q(Z).")))
true)
(dl-et-test!
"head var bound by body"
(dl-et-throws? (fn () (dl-program "p(X) :- q(X).")))
false)
(dl-et-test!
"head subset of body"
(dl-et-throws?
(fn
()
(dl-program
"edge(a,b). edge(b,c). reach(X, Z) :- edge(X, Y), edge(Y, Z).")))
false))))
(define
dl-eval-tests-run!
(fn
()
(do
(set! dl-et-pass 0)
(set! dl-et-fail 0)
(set! dl-et-failures (list))
(dl-et-run-all!)
{:failures dl-et-failures :total (+ dl-et-pass dl-et-fail) :passed dl-et-pass :failed dl-et-fail})))