;; lib/datalog/tests/negation.sx — stratified negation tests.

(define dl-nt-pass 0)
(define dl-nt-fail 0)
(define dl-nt-failures (list))

(define
  dl-nt-deep=?
  (fn
    (a b)
    (cond
      ((and (list? a) (list? b))
        (and (= (len a) (len b)) (dl-nt-deq-l? a b 0)))
      ((and (dict? a) (dict? b))
        (let ((ka (keys a)) (kb (keys b)))
          (and (= (len ka) (len kb)) (dl-nt-deq-d? a b ka 0))))
      ((and (number? a) (number? b)) (= a b))
      (else (equal? a b)))))

(define
  dl-nt-deq-l?
  (fn
    (a b i)
    (cond
      ((>= i (len a)) true)
      ((not (dl-nt-deep=? (nth a i) (nth b i))) false)
      (else (dl-nt-deq-l? a b (+ i 1))))))

(define
  dl-nt-deq-d?
  (fn
    (a b ka i)
    (cond
      ((>= i (len ka)) true)
      ((let ((k (nth ka i)))
         (not (dl-nt-deep=? (get a k) (get b k))))
        false)
      (else (dl-nt-deq-d? a b ka (+ i 1))))))

(define
  dl-nt-set=?
  (fn
    (a b)
    (and
      (= (len a) (len b))
      (dl-nt-subset? a b)
      (dl-nt-subset? b a))))

(define
  dl-nt-subset?
  (fn
    (xs ys)
    (cond
      ((= (len xs) 0) true)
      ((not (dl-nt-contains? ys (first xs))) false)
      (else (dl-nt-subset? (rest xs) ys)))))

(define
  dl-nt-contains?
  (fn
    (xs target)
    (cond
      ((= (len xs) 0) false)
      ((dl-nt-deep=? (first xs) target) true)
      (else (dl-nt-contains? (rest xs) target)))))

(define
  dl-nt-test!
  (fn
    (name got expected)
    (if
      (dl-nt-deep=? got expected)
      (set! dl-nt-pass (+ dl-nt-pass 1))
      (do
        (set! dl-nt-fail (+ dl-nt-fail 1))
        (append!
          dl-nt-failures
          (str
            name
            "\n    expected: " expected
            "\n    got:      " got))))))

(define
  dl-nt-test-set!
  (fn
    (name got expected)
    (if
      (dl-nt-set=? got expected)
      (set! dl-nt-pass (+ dl-nt-pass 1))
      (do
        (set! dl-nt-fail (+ dl-nt-fail 1))
        (append!
          dl-nt-failures
          (str
            name
            "\n    expected (set): " expected
            "\n    got:            " got))))))

(define
  dl-nt-throws?
  (fn
    (thunk)
    (let
      ((threw false))
      (do
        (guard
          (e (#t (set! threw true)))
          (thunk))
        threw))))

(define
  dl-nt-run-all!
  (fn
    ()
    (do
      ;; Negation against EDB-only relation.
      (dl-nt-test-set! "not against EDB"
        (dl-query
          (dl-program
            "p(1). p(2). p(3). r(2).
             q(X) :- p(X), not(r(X)).")
          (list (quote q) (quote X)))
        (list {:X 1} {:X 3}))

      ;; Negation against derived relation — needs stratification.
      (dl-nt-test-set! "not against derived rel"
        (dl-query
          (dl-program
            "p(1). p(2). p(3). s(2).
             r(X) :- s(X).
             q(X) :- p(X), not(r(X)).")
          (list (quote q) (quote X)))
        (list {:X 1} {:X 3}))

      ;; Two-step strata: r derives via s; q derives via not r.
      (dl-nt-test-set! "two-step strata"
        (dl-query
          (dl-program
            "node(a). node(b). node(c). node(d).
             edge(a, b). edge(b, c). edge(c, a).
             reach(X, Y) :- edge(X, Y).
             reach(X, Z) :- edge(X, Y), reach(Y, Z).
             unreachable(X) :- node(X), not(reach(a, X)).")
          (list (quote unreachable) (quote X)))
        (list {:X (quote d)}))

      ;; Combine negation with arithmetic and comparison.
      (dl-nt-test-set! "negation with arithmetic"
        (dl-query
          (dl-program
            "n(1). n(2). n(3). n(4). n(5). odd(1). odd(3). odd(5).
             even(X) :- n(X), not(odd(X)).")
          (list (quote even) (quote X)))
        (list {:X 2} {:X 4}))

      ;; Empty negation result.
      (dl-nt-test-set! "negation always succeeds"
        (dl-query
          (dl-program
            "p(1). p(2). q(X) :- p(X), not(r(X)).")
          (list (quote q) (quote X)))
        (list {:X 1} {:X 2}))

      ;; Negation always fails.
      (dl-nt-test-set! "negation always fails"
        (dl-query
          (dl-program
            "p(1). p(2). r(1). r(2). q(X) :- p(X), not(r(X)).")
          (list (quote q) (quote X)))
        (list))

      ;; Anonymous `_` in a negated literal is existentially quantified
      ;; — it doesn't need to be bound by an earlier body lit. Without
      ;; this exemption the safety check would reject the common idiom
      ;; `orphan(X) :- person(X), not(parent(X, _))`.
      (dl-nt-test-set! "negation with anonymous var — orphan idiom"
        (dl-query
          (dl-program
            "person(a). person(b). person(c). parent(a, b).
             orphan(X) :- person(X), not(parent(X, _)).")
          (list (quote orphan) (quote X)))
        (list {:X (quote b)} {:X (quote c)}))

      ;; Multiple anonymous vars are each independently existential.
      (dl-nt-test-set! "negation with multiple anonymous vars"
        (dl-query
          (dl-program
            "u(a). u(b). u(c). edge(a, x). edge(b, y).
             solo(X) :- u(X), not(edge(X, _)).")
          (list (quote solo) (quote X)))
        (list {:X (quote c)}))

      ;; Stratifiability checks.
      (dl-nt-test! "non-stratifiable rejected"
        (dl-nt-throws?
          (fn ()
            (let ((db (dl-make-db)))
              (do
                (dl-add-rule!
                  db
                  {:head (list (quote p) (quote X))
                   :body (list (list (quote q) (quote X))
                               {:neg (list (quote r) (quote X))})})
                (dl-add-rule!
                  db
                  {:head (list (quote r) (quote X))
                   :body (list (list (quote p) (quote X)))})
                (dl-add-fact! db (list (quote q) 1))
                (dl-saturate! db)))))
        true)

      (dl-nt-test! "stratifiable accepted"
        (dl-nt-throws?
          (fn ()
            (dl-program
              "p(1). p(2). r(2).
               q(X) :- p(X), not(r(X)).")))
        false)

      ;; Multi-stratum chain.
      (dl-nt-test-set! "three-level strata"
        (dl-query
          (dl-program
            "a(1). a(2). a(3). a(4).
             b(X) :- a(X), not(c(X)).
             c(X) :- d(X).
             d(2).
             d(4).")
          (list (quote b) (quote X)))
        (list {:X 1} {:X 3}))

      ;; Safety violation: negation refers to unbound var.
      (dl-nt-test! "negation safety violation"
        (dl-nt-throws?
          (fn ()
            (dl-program
              "p(1). q(X) :- p(X), not(r(Y)).")))
        true))))

(define
  dl-negation-tests-run!
  (fn
    ()
    (do
      (set! dl-nt-pass 0)
      (set! dl-nt-fail 0)
      (set! dl-nt-failures (list))
      (dl-nt-run-all!)
      {:passed dl-nt-pass
       :failed dl-nt-fail
       :total (+ dl-nt-pass dl-nt-fail)
       :failures dl-nt-failures})))
