;; lib/datalog/unify.sx — unification + substitution for Datalog terms.
;;
;; Term taxonomy (after parsing):
;;   variable  — SX symbol whose first char is uppercase A–Z or '_'.
;;   constant  — SX symbol whose first char is lowercase a–z (atom name).
;;   number    — numeric literal.
;;   string    — string literal.
;;   compound  — SX list (functor arg ... arg). In core Datalog these
;;               only appear as arithmetic expressions (see Phase 4
;;               safety analysis); compound-against-compound unification
;;               is supported anyway for completeness.
;;
;; Substitutions are immutable dicts keyed by variable name (string).
;; A failed unification returns nil; success returns the extended subst.

(define dl-empty-subst (fn () {}))

(define
  dl-var?
  (fn
    (term)
    (and
      (symbol? term)
      (let
        ((name (symbol->string term)))
        (and
          (> (len name) 0)
          (let
            ((c (slice name 0 1)))
            (or (and (>= c "A") (<= c "Z")) (= c "_"))))))))

;; Walk: chase variable bindings until we hit a non-variable or an unbound
;; variable. The result is either a non-variable term or an unbound var.
(define
  dl-walk
  (fn (term subst) (dl-walk-aux term subst (list))))

;; Internal: walk with a visited-var set so circular substitutions
;; (from raw dl-bind misuse) don't infinite-loop. Cycles return the
;; current term unchanged.
(define
  dl-walk-aux
  (fn
    (term subst visited)
    (if
      (dl-var? term)
      (let
        ((name (symbol->string term)))
        (cond
          ((dl-member? name visited) term)
          ((and (dict? subst) (has-key? subst name))
            (let ((seen (list)))
              (do
                (for-each (fn (v) (append! seen v)) visited)
                (append! seen name)
                (dl-walk-aux (get subst name) subst seen))))
          (else term)))
      term)))

;; Bind a variable symbol to a value in subst, returning a new subst.
(define
  dl-bind
  (fn (var-sym value subst) (assoc subst (symbol->string var-sym) value)))

(define
  dl-unify
  (fn
    (t1 t2 subst)
    (if
      (nil? subst)
      nil
      (let
        ((u1 (dl-walk t1 subst)) (u2 (dl-walk t2 subst)))
        (cond
          ((dl-var? u1)
            (cond
              ((and (dl-var? u2) (= (symbol->string u1) (symbol->string u2)))
                subst)
              (else (dl-bind u1 u2 subst))))
          ((dl-var? u2) (dl-bind u2 u1 subst))
          ((and (list? u1) (list? u2))
            (if
              (= (len u1) (len u2))
              (dl-unify-list u1 u2 subst 0)
              nil))
          ((and (number? u1) (number? u2)) (if (= u1 u2) subst nil))
          ((and (string? u1) (string? u2)) (if (= u1 u2) subst nil))
          ((and (symbol? u1) (symbol? u2))
            (if (= (symbol->string u1) (symbol->string u2)) subst nil))
          (else nil))))))

(define
  dl-unify-list
  (fn
    (a b subst i)
    (cond
      ((nil? subst) nil)
      ((>= i (len a)) subst)
      (else
        (dl-unify-list
          a
          b
          (dl-unify (nth a i) (nth b i) subst)
          (+ i 1))))))

;; Apply substitution: walk the term and recurse into lists.
(define
  dl-apply-subst
  (fn
    (term subst)
    (let
      ((w (dl-walk term subst)))
      (if (list? w) (map (fn (x) (dl-apply-subst x subst)) w) w))))

;; Ground? — true iff no free variables remain after walking.
(define
  dl-ground?
  (fn
    (term subst)
    (let
      ((w (dl-walk term subst)))
      (cond
        ((dl-var? w) false)
        ((list? w) (dl-ground-list? w subst 0))
        (else true)))))

(define
  dl-ground-list?
  (fn
    (xs subst i)
    (cond
      ((>= i (len xs)) true)
      ((not (dl-ground? (nth xs i) subst)) false)
      (else (dl-ground-list? xs subst (+ i 1))))))

;; Return the list of variable names appearing in a term (deduped, in
;; left-to-right order). Useful for safety analysis later.
(define
  dl-vars-of
  (fn (term) (let ((seen (list))) (do (dl-vars-of-aux term seen) seen))))

(define
  dl-vars-of-aux
  (fn
    (term acc)
    (cond
      ((dl-var? term)
        (let
          ((name (symbol->string term)))
          (when (not (dl-member? name acc)) (append! acc name))))
      ((list? term) (dl-vars-of-list term acc 0))
      (else nil))))

(define
  dl-vars-of-list
  (fn
    (xs acc i)
    (when
      (< i (len xs))
      (do
        (dl-vars-of-aux (nth xs i) acc)
        (dl-vars-of-list xs acc (+ i 1))))))

(define
  dl-member?
  (fn
    (x xs)
    (cond
      ((= (len xs) 0) false)
      ((= (first xs) x) true)
      (else (dl-member? x (rest xs))))))
