;; lib/datalog/tests/semi_naive.sx — semi-naive correctness vs naive. ;; ;; Strategy: differential — run both saturators on each program and ;; compare the resulting per-relation tuple counts. Counting (not ;; element-wise set equality) keeps the suite fast under the bundled ;; conformance session; correctness on the inhabitants is covered by ;; eval.sx and builtins.sx (which use dl-saturate! by default — the ;; semi-naive saturator). (define dl-sn-pass 0) (define dl-sn-fail 0) (define dl-sn-failures (list)) (define dl-sn-test! (fn (name got expected) (if (equal? got expected) (set! dl-sn-pass (+ dl-sn-pass 1)) (do (set! dl-sn-fail (+ dl-sn-fail 1)) (append! dl-sn-failures (str name "\n expected: " expected "\n got: " got)))))) ;; Load `source` into both a semi-naive and a naive db and return a ;; list of (rel-name semi-count naive-count) triples. Both sets must ;; have the same union of relation names. (define dl-sn-counts (fn (source) (let ((db-s (dl-program source)) (db-n (dl-program source))) (do (dl-saturate! db-s) (dl-saturate-naive! db-n) (let ((out (list))) (do (for-each (fn (k) (append! out (list k (len (dl-relation db-s k)) (len (dl-relation db-n k))))) (keys (get db-s :facts))) out)))))) (define dl-sn-counts-agree? (fn (counts) (cond ((= (len counts) 0) true) (else (let ((row (first counts))) (and (= (nth row 1) (nth row 2)) (dl-sn-counts-agree? (rest counts)))))))) (define dl-sn-chain-source (fn (n) (let ((parts (list ""))) (do (define dl-sn-loop (fn (i) (when (< i n) (do (append! parts (str "parent(" i ", " (+ i 1) "). ")) (dl-sn-loop (+ i 1)))))) (dl-sn-loop 0) (str (join "" parts) "ancestor(X, Y) :- parent(X, Y). " "ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))))) (define dl-sn-run-all! (fn () (do (dl-sn-test! "ancestor closure counts match" (dl-sn-counts-agree? (dl-sn-counts "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).")) true) (dl-sn-test! "cyclic reach counts match" (dl-sn-counts-agree? (dl-sn-counts "edge(1, 2). edge(2, 3). edge(3, 1). edge(3, 4).\n reach(X, Y) :- edge(X, Y).\n reach(X, Z) :- edge(X, Y), reach(Y, Z).")) true) (dl-sn-test! "same-gen counts match" (dl-sn-counts-agree? (dl-sn-counts "parent(a, b). parent(a, c). parent(b, d). parent(c, e).\n person(a). person(b). person(c). person(d). person(e).\n sg(X, X) :- person(X).\n sg(X, Y) :- parent(P1, X), sg(P1, P2), parent(P2, Y).")) true) (dl-sn-test! "rules with builtins counts match" (dl-sn-counts-agree? (dl-sn-counts "n(1). n(2). n(3). n(4). n(5).\n small(X) :- n(X), <(X, 5).\n succ(X, Y) :- n(X), <(X, 5), is(Y, +(X, 1)).")) true) (dl-sn-test! "static rule fires under semi-naive" (dl-sn-counts-agree? (dl-sn-counts "p(a). p(b). q(X) :- p(X), =(X, a).")) true) ;; Chain length 12 — multiple semi-naive iterations against ;; the recursive ancestor rule (differential vs naive). (dl-sn-test! "chain-12 ancestor counts match" (dl-sn-counts-agree? (dl-sn-counts (dl-sn-chain-source 12))) true) ;; Chain length 25 — semi-naive only — first-arg index makes ;; this tractable in conformance budget. (dl-sn-test! "chain-25 ancestor count value (semi only)" (let ((db (dl-program (dl-sn-chain-source 25)))) (do (dl-saturate! db) (len (dl-relation db "ancestor")))) 325) (dl-sn-test! "query through semi saturate" (let ((db (dl-program "parent(a, b). parent(b, c).\n ancestor(X, Y) :- parent(X, Y).\n ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))) (len (dl-query db (list (quote ancestor) (quote a) (quote X))))) 2)))) (define dl-semi-naive-tests-run! (fn () (do (set! dl-sn-pass 0) (set! dl-sn-fail 0) (set! dl-sn-failures (list)) (dl-sn-run-all!) {:failures dl-sn-failures :total (+ dl-sn-pass dl-sn-fail) :passed dl-sn-pass :failed dl-sn-fail})))