;; lib/datalog/tests/magic.sx — adornment + SIPS analysis tests. (define dl-mt-pass 0) (define dl-mt-fail 0) (define dl-mt-failures (list)) (define dl-mt-deep=? (fn (a b) (cond ((and (list? a) (list? b)) (and (= (len a) (len b)) (dl-mt-deq-l? a b 0))) ((and (dict? a) (dict? b)) (let ((ka (keys a)) (kb (keys b))) (and (= (len ka) (len kb)) (dl-mt-deq-d? a b ka 0)))) ((and (number? a) (number? b)) (= a b)) (else (equal? a b))))) (define dl-mt-deq-l? (fn (a b i) (cond ((>= i (len a)) true) ((not (dl-mt-deep=? (nth a i) (nth b i))) false) (else (dl-mt-deq-l? a b (+ i 1)))))) (define dl-mt-deq-d? (fn (a b ka i) (cond ((>= i (len ka)) true) ((let ((k (nth ka i))) (not (dl-mt-deep=? (get a k) (get b k)))) false) (else (dl-mt-deq-d? a b ka (+ i 1)))))) (define dl-mt-test! (fn (name got expected) (if (dl-mt-deep=? got expected) (set! dl-mt-pass (+ dl-mt-pass 1)) (do (set! dl-mt-fail (+ dl-mt-fail 1)) (append! dl-mt-failures (str name "\n expected: " expected "\n got: " got)))))) (define dl-mt-run-all! (fn () (do ;; Goal adornment. (dl-mt-test! "adorn 0-ary" (dl-adorn-goal (list (quote ready))) "") (dl-mt-test! "adorn all bound" (dl-adorn-goal (list (quote p) 1 2 3)) "bbb") (dl-mt-test! "adorn all free" (dl-adorn-goal (list (quote p) (quote X) (quote Y))) "ff") (dl-mt-test! "adorn mixed" (dl-adorn-goal (list (quote ancestor) (quote tom) (quote X))) "bf") (dl-mt-test! "adorn const var const" (dl-adorn-goal (list (quote p) (quote a) (quote X) (quote b))) "bfb") ;; dl-adorn-lit with explicit bound set. (dl-mt-test! "adorn lit with bound" (dl-adorn-lit (list (quote p) (quote X) (quote Y)) (list "X")) "bf") ;; Rule SIPS — chain ancestor. (dl-mt-test! "sips chain ancestor bf" (dl-rule-sips {:head (list (quote ancestor) (quote X) (quote Z)) :body (list (list (quote parent) (quote X) (quote Y)) (list (quote ancestor) (quote Y) (quote Z)))} "bf") (list {:lit (list (quote parent) (quote X) (quote Y)) :adornment "bf"} {:lit (list (quote ancestor) (quote Y) (quote Z)) :adornment "bf"})) ;; SIPS — head fully bound. (dl-mt-test! "sips head bb" (dl-rule-sips {:head (list (quote q) (quote X) (quote Y)) :body (list (list (quote p) (quote X) (quote Z)) (list (quote r) (quote Z) (quote Y)))} "bb") (list {:lit (list (quote p) (quote X) (quote Z)) :adornment "bf"} {:lit (list (quote r) (quote Z) (quote Y)) :adornment "bb"})) ;; SIPS — comparison; vars must be bound by prior body lit. (dl-mt-test! "sips with comparison" (dl-rule-sips {:head (list (quote q) (quote X)) :body (list (list (quote p) (quote X)) (list (string->symbol "<") (quote X) 5))} "f") (list {:lit (list (quote p) (quote X)) :adornment "f"} {:lit (list (string->symbol "<") (quote X) 5) :adornment "bb"})) ;; SIPS — `is` binds its left arg. (dl-mt-test! "sips with is" (dl-rule-sips {:head (list (quote q) (quote X) (quote Y)) :body (list (list (quote p) (quote X)) (list (quote is) (quote Y) (list (string->symbol "+") (quote X) 1)))} "ff") (list {:lit (list (quote p) (quote X)) :adornment "f"} {:lit (list (quote is) (quote Y) (list (string->symbol "+") (quote X) 1)) :adornment "fb"})) ;; Magic predicate naming. (dl-mt-test! "magic-rel-name" (dl-magic-rel-name "ancestor" "bf") "magic_ancestor^bf") ;; Bound-args extraction. (dl-mt-test! "bound-args bf" (dl-bound-args (list (quote ancestor) (quote tom) (quote X)) "bf") (list (quote tom))) (dl-mt-test! "bound-args mixed" (dl-bound-args (list (quote p) 1 (quote Y) 3) "bfb") (list 1 3)) (dl-mt-test! "bound-args all-free" (dl-bound-args (list (quote p) (quote X) (quote Y)) "ff") (list)) ;; Magic literal construction. (dl-mt-test! "magic-lit" (dl-magic-lit "ancestor" "bf" (list (quote tom))) (list (string->symbol "magic_ancestor^bf") (quote tom))) ;; Magic-sets rewriter: structural sanity. (dl-mt-test! "rewrite ancestor produces seed" (let ((rules (list {:head (list (quote ancestor) (quote X) (quote Y)) :body (list (list (quote parent) (quote X) (quote Y)))} {:head (list (quote ancestor) (quote X) (quote Z)) :body (list (list (quote parent) (quote X) (quote Y)) (list (quote ancestor) (quote Y) (quote Z)))}))) (get (dl-magic-rewrite rules "ancestor" "bf" (list (quote a))) :seed)) (list (string->symbol "magic_ancestor^bf") (quote a))) ;; Equivalence: rewritten program derives same ancestor tuples. ;; In a chain a→b→c→d, magic-rewritten run still derives all ;; ancestor pairs reachable from any node a/b/c/d propagated via ;; magic_ancestor^bf — i.e. the full closure (6 tuples). Magic ;; saves work only when the EDB has irrelevant nodes outside ;; the seed's transitive cone. (dl-mt-test! "magic-rewritten ancestor count" (let ((rules (list {:head (list (quote ancestor) (quote X) (quote Y)) :body (list (list (quote parent) (quote X) (quote Y)))} {:head (list (quote ancestor) (quote X) (quote Z)) :body (list (list (quote parent) (quote X) (quote Y)) (list (quote ancestor) (quote Y) (quote Z)))})) (edb (list (list (quote parent) (quote a) (quote b)) (list (quote parent) (quote b) (quote c)) (list (quote parent) (quote c) (quote d))))) (let ((rewritten (dl-magic-rewrite rules "ancestor" "bf" (list (quote a)))) (db (dl-make-db))) (do (for-each (fn (f) (dl-add-fact! db f)) edb) (dl-add-fact! db (get rewritten :seed)) (for-each (fn (r) (dl-add-rule! db r)) (get rewritten :rules)) (dl-saturate! db) (len (dl-relation db "ancestor"))))) 6) ;; dl-magic-query: end-to-end driver, doesn't mutate caller's db. ;; dl-magic-query falls back to dl-query for built-in, ;; aggregate, and negation goals (the magic seed would ;; otherwise be non-ground). (dl-mt-test! "magic-query falls back on aggregate" (let ((r (dl-magic-query (dl-program "p(1). p(2). p(3).") (list (quote count) (quote N) (quote X) (list (quote p) (quote X)))))) (and (= (len r) 1) (= (get (first r) "N") 3))) true) (dl-mt-test! "magic-query equivalent to dl-query" (let ((db (dl-program "parent(a, b). parent(b, c). parent(c, d). ancestor(X, Y) :- parent(X, Y). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))) (let ((semi (dl-query db (list (quote ancestor) (quote a) (quote X)))) (magic (dl-magic-query db (list (quote ancestor) (quote a) (quote X))))) (= (len semi) (len magic)))) true) ;; magic-query doesn't mutate caller db. (dl-mt-test! "magic-query preserves caller db" (let ((db (dl-program "parent(a, b). parent(b, c). ancestor(X, Y) :- parent(X, Y). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))) (let ((rules-before (len (dl-rules db)))) (do (dl-magic-query db (list (quote ancestor) (quote a) (quote X))) (= rules-before (len (dl-rules db)))))) true) ;; Magic-sets benefit: query touches only one cluster of a ;; multi-component graph. Semi-naive derives the full closure ;; over both clusters; magic only the seeded one. ;; Magic-vs-semi work shape: chain of 12. Semi-naive ;; derives the full closure (78 = 12·13/2). A magic query ;; rooted at node 0 returns the 12 descendants only — ;; demonstrating that magic limits derivation to the ;; query's transitive cone. (dl-mt-test! "magic vs semi work-shape on chain-12" (let ((source (str "parent(0, 1). parent(1, 2). parent(2, 3). " "parent(3, 4). parent(4, 5). parent(5, 6). " "parent(6, 7). parent(7, 8). parent(8, 9). " "parent(9, 10). parent(10, 11). parent(11, 12). " "ancestor(X, Y) :- parent(X, Y). " "ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))) (let ((db1 (dl-make-db)) (db2 (dl-make-db))) (do (dl-load-program! db1 source) (dl-saturate! db1) (dl-load-program! db2 source) (let ((semi-count (len (dl-relation db1 "ancestor"))) (magic-count (len (dl-magic-query db2 (list (quote ancestor) 0 (quote X)))))) ;; Magic returns only descendants of 0 (12 of them). (and (= semi-count 78) (= magic-count 12)))))) true) ;; Magic + arithmetic: rules with `is` clauses pass through ;; the rewriter unchanged (built-ins aren't propagated). (dl-mt-test! "magic preserves arithmetic" (let ((source "n(1). n(2). n(3). doubled(X, Y) :- n(X), is(Y, *(X, 2)).")) (let ((semi (dl-eval source "?- doubled(2, Y).")) (magic (dl-eval-magic source "?- doubled(2, Y)."))) (= (len semi) (len magic)))) true) (dl-mt-test! "magic skips irrelevant clusters" (let ;; Two disjoint chains. Query is rooted in cluster 1. ((db (dl-program "parent(a, b). parent(b, c). parent(x, y). parent(y, z). ancestor(X, Y) :- parent(X, Y). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))) (do (dl-saturate! db) (let ((semi-count (len (dl-relation db "ancestor"))) (magic-results (dl-magic-query db (list (quote ancestor) (quote a) (quote X))))) ;; Semi-naive derives 6 (3 in each cluster). Magic ;; gives 3 query results (a's reachable: b, c). (and (= semi-count 6) (= (len magic-results) 2))))) true) (dl-mt-test! "magic-rewritten finds same answers" (let ((rules (list {:head (list (quote ancestor) (quote X) (quote Y)) :body (list (list (quote parent) (quote X) (quote Y)))} {:head (list (quote ancestor) (quote X) (quote Z)) :body (list (list (quote parent) (quote X) (quote Y)) (list (quote ancestor) (quote Y) (quote Z)))})) (edb (list (list (quote parent) (quote a) (quote b)) (list (quote parent) (quote b) (quote c))))) (let ((rewritten (dl-magic-rewrite rules "ancestor" "bf" (list (quote a)))) (db (dl-make-db))) (do (for-each (fn (f) (dl-add-fact! db f)) edb) (dl-add-fact! db (get rewritten :seed)) (for-each (fn (r) (dl-add-rule! db r)) (get rewritten :rules)) (dl-saturate! db) (len (dl-query db (list (quote ancestor) (quote a) (quote X))))))) 2)))) (define dl-magic-tests-run! (fn () (do (set! dl-mt-pass 0) (set! dl-mt-fail 0) (set! dl-mt-failures (list)) (dl-mt-run-all!) {:passed dl-mt-pass :failed dl-mt-fail :total (+ dl-mt-pass dl-mt-fail) :failures dl-mt-failures})))