Files
rose-ash/lib/datalog/unify.sx
giles 9a16f27075
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 27s
datalog: dl-walk handles circular substitutions without infinite loop (257/257)
Bug: dl-walk would infinite-loop on a circular substitution
(e.g. A→B and B→A simultaneously). The walk endlessly chased
the cycle. This couldn't be produced through dl-unify (which has
cycle-safe behavior via existing bindings), but raw dl-bind calls
or external manipulation of the subst dict could create it.

Fix: dl-walk now threads a visited-names list through the
recursion. If a variable name is already in the list, the walk
stops and returns the current term unchanged. Normal chained
walks are unaffected (A→B→C→42 still resolves to 42).

1 new unify test verifies circular substitutions don't hang.
2026-05-11 07:20:20 +00:00

172 lines
4.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;; lib/datalog/unify.sx — unification + substitution for Datalog terms.
;;
;; Term taxonomy (after parsing):
;; variable — SX symbol whose first char is uppercase AZ or '_'.
;; constant — SX symbol whose first char is lowercase az (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))))))