;; lib/datalog/tests/parse.sx — parser unit tests
;;
;; Run via: bash lib/datalog/conformance.sh
;; Or:      (load "lib/datalog/tokenizer.sx") (load "lib/datalog/parser.sx")
;;          (load "lib/datalog/tests/parse.sx") (dl-parse-tests-run!)

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

;; Order-independent structural equality. Lists compared positionally,
;; dicts as sets of (key, value) pairs. Numbers via = (so 30.0 = 30).
(define
  dl-deep-equal?
  (fn
    (a b)
    (cond
      ((and (list? a) (list? b))
        (and (= (len a) (len b)) (dl-deep-equal-list? a b 0)))
      ((and (dict? a) (dict? b))
        (let
          ((ka (keys a)) (kb (keys b)))
          (and
            (= (len ka) (len kb))
            (dl-deep-equal-dict? a b ka 0))))
      ((and (number? a) (number? b)) (= a b))
      (else (equal? a b)))))

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

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

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

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

(define
  dl-pt-run-all!
  (fn
    ()
    (do
      (dl-pt-test! "empty program" (dl-parse "") (list))
      (dl-pt-test! "fact" (dl-parse "parent(tom, bob).") (list {:body (list) :head (list (quote parent) (quote tom) (quote bob))}))
      (dl-pt-test!
        "two facts"
        (dl-parse "parent(tom, bob). parent(bob, ann).")
        (list {:body (list) :head (list (quote parent) (quote tom) (quote bob))} {:body (list) :head (list (quote parent) (quote bob) (quote ann))}))
      (dl-pt-test! "zero-ary fact" (dl-parse "ready.") (list {:body (list) :head (list (quote ready))}))
      (dl-pt-test!
        "rule one body lit"
        (dl-parse "ancestor(X, Y) :- parent(X, Y).")
        (list {:body (list (list (quote parent) (quote X) (quote Y))) :head (list (quote ancestor) (quote X) (quote Y))}))
      (dl-pt-test!
        "recursive rule"
        (dl-parse "ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")
        (list {:body (list (list (quote parent) (quote X) (quote Y)) (list (quote ancestor) (quote Y) (quote Z))) :head (list (quote ancestor) (quote X) (quote Z))}))
      (dl-pt-test!
        "query single"
        (dl-parse "?- ancestor(tom, X).")
        (list {:query (list (list (quote ancestor) (quote tom) (quote X)))}))
      (dl-pt-test!
        "query multi"
        (dl-parse "?- p(X), q(X).")
        (list {:query (list (list (quote p) (quote X)) (list (quote q) (quote X)))}))
      (dl-pt-test!
        "negation"
        (dl-parse "safe(X) :- person(X), not(parent(X, _)).")
        (list {:body (list (list (quote person) (quote X)) {:neg (list (quote parent) (quote X) (quote _))}) :head (list (quote safe) (quote X))}))
      (dl-pt-test!
        "number arg"
        (dl-parse "age(alice, 30).")
        (list {:body (list) :head (list (quote age) (quote alice) 30)}))
      (dl-pt-test!
        "string arg"
        (dl-parse "label(x, \"hi\").")
        (list {:body (list) :head (list (quote label) (quote x) "hi")}))
      ;; Quoted 'atoms' parse as strings — a uppercase-starting name
      ;; in quotes used to misclassify as a variable and reject the
      ;; fact as non-ground.
      (dl-pt-test!
        "quoted atom arg parses as string"
        (dl-parse "p('Hello World').")
        (list {:body (list) :head (list (quote p) "Hello World")}))
      (dl-pt-test!
        "comparison literal"
        (dl-parse "p(X) :- <(X, 5).")
        (list {:body (list (list (string->symbol "<") (quote X) 5)) :head (list (quote p) (quote X))}))
      (dl-pt-test!
        "is with arith"
        (dl-parse "succ(X, Y) :- nat(X), is(Y, +(X, 1)).")
        (list {:body (list (list (quote nat) (quote X)) (list (quote is) (quote Y) (list (string->symbol "+") (quote X) 1))) :head (list (quote succ) (quote X) (quote Y))}))
      (dl-pt-test!
        "mixed program"
        (dl-parse "p(a). p(b). q(X) :- p(X). ?- q(Y).")
        (list {:body (list) :head (list (quote p) (quote a))} {:body (list) :head (list (quote p) (quote b))} {:body (list (list (quote p) (quote X))) :head (list (quote q) (quote X))} {:query (list (list (quote q) (quote Y)))}))
      (dl-pt-test!
        "comments skipped"
        (dl-parse "% comment\nfoo(a).\n/* block */ bar(b).")
        (list {:body (list) :head (list (quote foo) (quote a))} {:body (list) :head (list (quote bar) (quote b))}))
      (dl-pt-test!
        "underscore var"
        (dl-parse "p(X) :- q(X, _).")
        (list {:body (list (list (quote q) (quote X) (quote _))) :head (list (quote p) (quote X))}))
      ;; Negative number literals parse as one negative number,
      ;; while subtraction (`-(X, Y)`) compound is preserved.
      (dl-pt-test!
        "negative integer literal"
        (dl-parse "n(-3).")
        (list {:head (list (quote n) -3) :body (list)}))

      (dl-pt-test!
        "subtraction compound preserved"
        (dl-parse "r(X) :- is(X, -(10, 3)).")
        (list
          {:head (list (quote r) (quote X))
           :body (list (list (quote is) (quote X)
                             (list (string->symbol "-") 10 3)))}))

      (dl-pt-test!
        "number as relation name raises"
        (dl-pt-throws? (fn () (dl-parse "1(X) :- p(X).")))
        true)

      (dl-pt-test!
        "var as relation name raises"
        (dl-pt-throws? (fn () (dl-parse "P(X).")))
        true)

      (dl-pt-test!
        "missing dot raises"
        (dl-pt-throws? (fn () (dl-parse "p(a)")))
        true)
      (dl-pt-test!
        "trailing comma raises"
        (dl-pt-throws? (fn () (dl-parse "p(a,).")))
        true))))

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