Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 22s
End-to-end magic-sets entry point. Given (db, query-goal):
- copies the caller's EDB facts (relations not headed by any
rule) into a fresh internal db
- adds the magic seed fact
- adds the rewritten rules
- saturates and runs the query
- returns the substitution list
Caller's db is untouched. Equivalent to dl-query for any
fully-stratifiable program; intended as a perf alternative on
goal-shaped queries against large recursive relations.
2 new tests: equivalence to dl-query on chain-3 ancestor, and
non-mutation of the caller's db (rules count unchanged).
264 lines
9.2 KiB
Plaintext
264 lines
9.2 KiB
Plaintext
;; 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-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)
|
|
|
|
(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})))
|