;; lib/datalog/parser.sx — Datalog tokens → AST
;;
;; Output shapes:
;;   Literal (positive)  := (relname arg ... arg)        — SX list
;;   Literal (negative)  := {:neg (relname arg ... arg)} — dict
;;   Argument            := var-symbol | atom-symbol | number | string
;;                        | (op-name arg ... arg)        — arithmetic compound
;;   Fact                := {:head literal :body ()}
;;   Rule                := {:head literal :body (lit ... lit)}
;;   Query               := {:query (lit ... lit)}
;;   Program             := list of facts / rules / queries
;;
;; Variables and constants are both SX symbols; the evaluator dispatches
;; on first-char case ('A'..'Z' or '_' = variable, otherwise constant).
;;
;; The parser permits nested compounds in arg position to support
;; arithmetic (e.g. (is Z (+ X Y))). Safety analysis at rule-load time
;; rejects compounds whose head is not an arithmetic operator.

(define
  dl-pp-peek
  (fn
    (st)
    (let
      ((i (get st :idx)) (tokens (get st :tokens)))
      (if (< i (len tokens)) (nth tokens i) {:type "eof" :value nil :pos 0}))))

(define
  dl-pp-peek2
  (fn
    (st)
    (let
      ((i (+ (get st :idx) 1)) (tokens (get st :tokens)))
      (if (< i (len tokens)) (nth tokens i) {:type "eof" :value nil :pos 0}))))

(define
  dl-pp-advance!
  (fn (st) (dict-set! st :idx (+ (get st :idx) 1))))

(define
  dl-pp-at?
  (fn
    (st type value)
    (let
      ((t (dl-pp-peek st)))
      (and
        (= (get t :type) type)
        (or (= value nil) (= (get t :value) value))))))

(define
  dl-pp-error
  (fn
    (st msg)
    (let
      ((t (dl-pp-peek st)))
      (error
        (str
          "Parse error at pos "
          (get t :pos)
          ": "
          msg
          " (got "
          (get t :type)
          " '"
          (if (= (get t :value) nil) "" (get t :value))
          "')")))))

(define
  dl-pp-expect!
  (fn
    (st type value)
    (let
      ((t (dl-pp-peek st)))
      (if
        (dl-pp-at? st type value)
        (do (dl-pp-advance! st) t)
        (dl-pp-error
          st
          (str "expected " type (if (= value nil) "" (str " '" value "'"))))))))

;; Argument: variable, atom, number, string, or compound (relname/op + parens).
(define
  dl-pp-parse-arg
  (fn
    (st)
    (let
      ((t (dl-pp-peek st)))
      (let
        ((ty (get t :type)) (vv (get t :value)))
        (cond
          ((= ty "number") (do (dl-pp-advance! st) vv))
          ((= ty "string") (do (dl-pp-advance! st) vv))
          ((= ty "var") (do (dl-pp-advance! st) (string->symbol vv)))
          ;; Negative numeric literal: `-` op directly followed by a
          ;; number (no `(`) is parsed as a single negative number.
          ;; This keeps `(-X Y)` (compound) and `-N` (literal) distinct.
          ((and (= ty "op") (= vv "-")
                (= (get (dl-pp-peek2 st) :type) "number"))
            (do
              (dl-pp-advance! st)
              (let
                ((n (get (dl-pp-peek st) :value)))
                (do (dl-pp-advance! st) (- 0 n)))))
          ((or (= ty "atom") (= ty "op"))
            (do
              (dl-pp-advance! st)
              (if
                (dl-pp-at? st "punct" "(")
                (do
                  (dl-pp-advance! st)
                  (let
                    ((args (dl-pp-parse-arg-list st)))
                    (do
                      (dl-pp-expect! st "punct" ")")
                      (cons (string->symbol vv) args))))
                (string->symbol vv))))
          (else (dl-pp-error st "expected term")))))))

;; Comma-separated args inside parens.
(define
  dl-pp-parse-arg-list
  (fn
    (st)
    (let
      ((args (list)))
      (do
        (append! args (dl-pp-parse-arg st))
        (define
          dl-pp-arg-loop
          (fn
            ()
            (when
              (dl-pp-at? st "punct" ",")
              (do
                (dl-pp-advance! st)
                (append! args (dl-pp-parse-arg st))
                (dl-pp-arg-loop)))))
        (dl-pp-arg-loop)
        args))))

;; A positive literal: relname (atom or op) followed by optional (args).
(define
  dl-pp-parse-positive
  (fn
    (st)
    (let
      ((t (dl-pp-peek st)))
      (let
        ((ty (get t :type)) (vv (get t :value)))
        (if
          (or (= ty "atom") (= ty "op"))
          (do
            (dl-pp-advance! st)
            (if
              (dl-pp-at? st "punct" "(")
              (do
                (dl-pp-advance! st)
                (let
                  ((args (dl-pp-parse-arg-list st)))
                  (do
                    (dl-pp-expect! st "punct" ")")
                    (cons (string->symbol vv) args))))
              (list (string->symbol vv))))
          (dl-pp-error st "expected literal head"))))))

;; A body literal: positive, or not(positive).
(define
  dl-pp-parse-body-lit
  (fn
    (st)
    (let
      ((t1 (dl-pp-peek st)) (t2 (dl-pp-peek2 st)))
      (if
        (and
          (= (get t1 :type) "atom")
          (= (get t1 :value) "not")
          (= (get t2 :type) "punct")
          (= (get t2 :value) "("))
        (do
          (dl-pp-advance! st)
          (dl-pp-advance! st)
          (let
            ((inner (dl-pp-parse-positive st)))
            (do (dl-pp-expect! st "punct" ")") {:neg inner})))
        (dl-pp-parse-positive st)))))

;; Comma-separated body literals.
(define
  dl-pp-parse-body
  (fn
    (st)
    (let
      ((lits (list)))
      (do
        (append! lits (dl-pp-parse-body-lit st))
        (define
          dl-pp-body-loop
          (fn
            ()
            (when
              (dl-pp-at? st "punct" ",")
              (do
                (dl-pp-advance! st)
                (append! lits (dl-pp-parse-body-lit st))
                (dl-pp-body-loop)))))
        (dl-pp-body-loop)
        lits))))

;; Single clause: fact, rule, or query. Consumes trailing dot.
(define
  dl-pp-parse-clause
  (fn
    (st)
    (cond
      ((dl-pp-at? st "op" "?-")
        (do
          (dl-pp-advance! st)
          (let
            ((body (dl-pp-parse-body st)))
            (do (dl-pp-expect! st "punct" ".") {:query body}))))
      (else
        (let
          ((head (dl-pp-parse-positive st)))
          (cond
            ((dl-pp-at? st "op" ":-")
              (do
                (dl-pp-advance! st)
                (let
                  ((body (dl-pp-parse-body st)))
                  (do (dl-pp-expect! st "punct" ".") {:body body :head head}))))
            (else (do (dl-pp-expect! st "punct" ".") {:body (list) :head head}))))))))

(define
  dl-parse-program
  (fn
    (tokens)
    (let
      ((st {:tokens tokens :idx 0}) (clauses (list)))
      (do
        (define
          dl-pp-prog-loop
          (fn
            ()
            (when
              (not (dl-pp-at? st "eof" nil))
              (do
                (append! clauses (dl-pp-parse-clause st))
                (dl-pp-prog-loop)))))
        (dl-pp-prog-loop)
        clauses))))

(define dl-parse (fn (src) (dl-parse-program (dl-tokenize src))))
