;; lib/datalog/tests/builtins.sx — comparison + arithmetic body literals. (define dl-bt-pass 0) (define dl-bt-fail 0) (define dl-bt-failures (list)) (define dl-bt-deep=? (fn (a b) (cond ((and (list? a) (list? b)) (and (= (len a) (len b)) (dl-bt-deq-l? a b 0))) ((and (dict? a) (dict? b)) (let ((ka (keys a)) (kb (keys b))) (and (= (len ka) (len kb)) (dl-bt-deq-d? a b ka 0)))) ((and (number? a) (number? b)) (= a b)) (else (equal? a b))))) (define dl-bt-deq-l? (fn (a b i) (cond ((>= i (len a)) true) ((not (dl-bt-deep=? (nth a i) (nth b i))) false) (else (dl-bt-deq-l? a b (+ i 1)))))) (define dl-bt-deq-d? (fn (a b ka i) (cond ((>= i (len ka)) true) ((let ((k (nth ka i))) (not (dl-bt-deep=? (get a k) (get b k)))) false) (else (dl-bt-deq-d? a b ka (+ i 1)))))) (define dl-bt-set=? (fn (a b) (and (= (len a) (len b)) (dl-bt-subset? a b) (dl-bt-subset? b a)))) (define dl-bt-subset? (fn (xs ys) (cond ((= (len xs) 0) true) ((not (dl-bt-contains? ys (first xs))) false) (else (dl-bt-subset? (rest xs) ys))))) (define dl-bt-contains? (fn (xs target) (cond ((= (len xs) 0) false) ((dl-bt-deep=? (first xs) target) true) (else (dl-bt-contains? (rest xs) target))))) (define dl-bt-test-set! (fn (name got expected) (if (dl-bt-set=? got expected) (set! dl-bt-pass (+ dl-bt-pass 1)) (do (set! dl-bt-fail (+ dl-bt-fail 1)) (append! dl-bt-failures (str name "\n expected (set): " expected "\n got: " got)))))) (define dl-bt-test! (fn (name got expected) (if (dl-bt-deep=? got expected) (set! dl-bt-pass (+ dl-bt-pass 1)) (do (set! dl-bt-fail (+ dl-bt-fail 1)) (append! dl-bt-failures (str name "\n expected: " expected "\n got: " got)))))) (define dl-bt-throws? (fn (thunk) (let ((threw false)) (do (guard (e (#t (set! threw true))) (thunk)) threw)))) (define dl-bt-run-all! (fn () (do (dl-bt-test-set! "less than filter" (dl-query (dl-program "age(alice, 30). age(bob, 17). age(carol, 22).\n adult(X) :- age(X, A), >=(A, 18).") (list (quote adult) (quote X))) (list {:X (quote alice)} {:X (quote carol)})) (dl-bt-test-set! "less-equal filter" (dl-query (dl-program "n(1). n(2). n(3). n(4). n(5).\n small(X) :- n(X), <=(X, 3).") (list (quote small) (quote X))) (list {:X 1} {:X 2} {:X 3})) (dl-bt-test-set! "not-equal filter" (dl-query (dl-program "p(1, 2). p(2, 2). p(3, 4).\n diff(X, Y) :- p(X, Y), !=(X, Y).") (list (quote diff) (quote X) (quote Y))) (list {:X 1 :Y 2} {:X 3 :Y 4})) (dl-bt-test-set! "is plus" (dl-query (dl-program "n(1). n(2). n(3).\n succ(X, Y) :- n(X), is(Y, +(X, 1)).") (list (quote succ) (quote X) (quote Y))) (list {:X 1 :Y 2} {:X 2 :Y 3} {:X 3 :Y 4})) (dl-bt-test-set! "is multiply" (dl-query (dl-program "n(2). n(3). n(4).\n square(X, Y) :- n(X), is(Y, *(X, X)).") (list (quote square) (quote X) (quote Y))) (list {:X 2 :Y 4} {:X 3 :Y 9} {:X 4 :Y 16})) (dl-bt-test-set! "is nested expr" (dl-query (dl-program "n(1). n(2). n(3).\n f(X, Y) :- n(X), is(Y, *(+(X, 1), 2)).") (list (quote f) (quote X) (quote Y))) (list {:X 1 :Y 4} {:X 2 :Y 6} {:X 3 :Y 8})) (dl-bt-test-set! "is bound LHS — equality" (dl-query (dl-program "n(1, 2). n(2, 5). n(3, 4).\n succ(X, Y) :- n(X, Y), is(Y, +(X, 1)).") (list (quote succ) (quote X) (quote Y))) (list {:X 1 :Y 2} {:X 3 :Y 4})) (dl-bt-test-set! "triple via is" (dl-query (dl-program "n(1). n(2). n(3).\n triple(X, Y) :- n(X), is(Y, *(X, 3)).") (list (quote triple) (quote X) (quote Y))) (list {:X 1 :Y 3} {:X 2 :Y 6} {:X 3 :Y 9})) (dl-bt-test-set! "= unifies var with constant" (dl-query (dl-program "p(a). p(b).\n qual(X) :- p(X), =(X, a).") (list (quote qual) (quote X))) (list {:X (quote a)})) (dl-bt-test-set! "= unifies two vars (one bound)" (dl-query (dl-program "p(a). p(b).\n twin(X, Y) :- p(X), =(Y, X).") (list (quote twin) (quote X) (quote Y))) (list {:X (quote a) :Y (quote a)} {:X (quote b) :Y (quote b)})) (dl-bt-test! "big count" (let ((db (dl-program "n(0). n(1). n(2). n(3). n(4). n(5). n(6). n(7). n(8). n(9).\n big(X) :- n(X), >=(X, 5)."))) (do (dl-saturate! db) (len (dl-relation db "big")))) 5) ;; Built-in / arithmetic literals work as standalone query goals ;; (without needing a wrapper rule). (dl-bt-test-set! "comparison-only goal true" (dl-eval "" "?- <(1, 2).") (list {})) (dl-bt-test-set! "comparison-only goal false" (dl-eval "" "?- <(2, 1).") (list)) (dl-bt-test-set! "is goal binds" (dl-eval "" "?- is(N, +(2, 3)).") (list {:N 5})) ;; Bounded successor: a recursive rule with a comparison ;; guard terminates because the Herbrand base is effectively ;; bounded. (dl-bt-test-set! "bounded successor" (dl-query (dl-program "nat(0). nat(Y) :- nat(X), is(Y, +(X, 1)), <(Y, 5).") (list (quote nat) (quote X))) (list {:X 0} {:X 1} {:X 2} {:X 3} {:X 4})) (dl-bt-test! "unsafe — comparison without binder" (dl-bt-throws? (fn () (dl-program "p(X) :- <(X, 5)."))) true) (dl-bt-test! "unsafe — comparison both unbound" (dl-bt-throws? (fn () (dl-program "p(X, Y) :- <(X, Y), q(X)."))) true) (dl-bt-test! "unsafe — is uses unbound RHS var" (dl-bt-throws? (fn () (dl-program "p(X, Y) :- q(X), is(Y, +(X, Z))."))) true) (dl-bt-test! "unsafe — is on its own" (dl-bt-throws? (fn () (dl-program "p(Y) :- is(Y, +(X, 1))."))) true) (dl-bt-test! "unsafe — = between two unbound" (dl-bt-throws? (fn () (dl-program "p(X, Y) :- =(X, Y)."))) true) (dl-bt-test! "safe — is binds head var" (dl-bt-throws? (fn () (dl-program "n(1). p(Y) :- n(X), is(Y, +(X, 1))."))) false) (dl-bt-test! "safe — comparison after binder" (dl-bt-throws? (fn () (dl-program "n(1). big(X) :- n(X), >=(X, 0)."))) false) (dl-bt-test! "safe — = binds head var" (dl-bt-throws? (fn () (dl-program "p(a). p(b). x(Y) :- p(X), =(Y, X)."))) false) ;; Division by zero raises with a clear error. Without this guard ;; SX's `/` returned IEEE infinity, which then silently flowed ;; through comparisons and aggregations. (dl-bt-test! "is — division by zero raises" (dl-bt-throws? (fn () (dl-eval "p(10). q(R) :- p(X), is(R, /(X, 0))." "?- q(R)."))) true) ;; Comparison ops `<`, `<=`, `>`, `>=` require both operands to ;; have the same primitive type. Cross-type comparisons used to ;; silently return false (for some pairs) or raise a confusing ;; host-level error (for others) — now they all raise with a ;; message that names the offending values. (dl-bt-test! "comparison — string vs number raises" (dl-bt-throws? (fn () (dl-eval "p(\"hello\"). q(X) :- p(X), <(X, 5)." "?- q(X)."))) true) ;; `!=` is the exception — it's a polymorphic inequality test ;; (uses dl-tuple-equal? underneath) so cross-type pairs are ;; legitimate (and trivially unequal). (dl-bt-test-set! "!= works across types" (dl-query (dl-program "p(1). p(\"1\"). q(X) :- p(X), !=(X, 1).") (quote (q X))) (list {:X "1"}))))) (define dl-builtins-tests-run! (fn () (do (set! dl-bt-pass 0) (set! dl-bt-fail 0) (set! dl-bt-failures (list)) (dl-bt-run-all!) {:failures dl-bt-failures :total (+ dl-bt-pass dl-bt-fail) :passed dl-bt-pass :failed dl-bt-fail})))