;; lib/datalog/strata.sx — dependency graph, SCC analysis, stratum assignment. ;; ;; A program is stratifiable iff no cycle in its dependency graph passes ;; through a negative edge. The stratum of relation R is the depth at which ;; R can first be evaluated: ;; ;; stratum(R) = max over edges (R → S) of: ;; stratum(S) if the edge is positive ;; stratum(S) + 1 if the edge is negative ;; ;; All relations in the same SCC share a stratum (and the SCC must have only ;; positive internal edges, else the program is non-stratifiable). ;; Build dep graph: dict {head-rel-name -> ({:rel str :neg bool} ...)}. (define dl-build-dep-graph (fn (db) (let ((g {})) (do (for-each (fn (rule) (let ((head-rel (dl-rel-name (get rule :head)))) (when (not (nil? head-rel)) (do (when (not (has-key? g head-rel)) (dict-set! g head-rel (list))) (let ((existing (get g head-rel))) (for-each (fn (lit) (cond ((dl-aggregate? lit) (let ((edge (dl-aggregate-dep-edge lit))) (when (not (nil? edge)) (append! existing edge)))) (else (let ((target (cond ((and (dict? lit) (has-key? lit :neg)) (dl-rel-name (get lit :neg))) ((dl-builtin? lit) nil) ((and (list? lit) (> (len lit) 0)) (dl-rel-name lit)) (else nil))) (neg? (and (dict? lit) (has-key? lit :neg)))) (when (not (nil? target)) (append! existing {:rel target :neg neg?})))))) (get rule :body))))))) (dl-rules db)) g)))) ;; All relations referenced — heads of rules + EDB names + body relations. (define dl-all-relations (fn (db) (let ((seen (list))) (do (for-each (fn (k) (when (not (dl-member-string? k seen)) (append! seen k))) (keys (get db :facts))) (for-each (fn (rule) (do (let ((h (dl-rel-name (get rule :head)))) (when (and (not (nil? h)) (not (dl-member-string? h seen))) (append! seen h))) (for-each (fn (lit) (let ((t (cond ((dl-aggregate? lit) (let ((edge (dl-aggregate-dep-edge lit))) (if (nil? edge) nil (get edge :rel)))) ((and (dict? lit) (has-key? lit :neg)) (dl-rel-name (get lit :neg))) ((dl-builtin? lit) nil) ((and (list? lit) (> (len lit) 0)) (dl-rel-name lit)) (else nil)))) (when (and (not (nil? t)) (not (dl-member-string? t seen))) (append! seen t)))) (get rule :body)))) (dl-rules db)) seen)))) ;; reach: dict {from: dict {to: edge-info}} where edge-info is ;; {:any bool :neg bool} ;; meaning "any path from `from` to `to`" and "exists a negative-passing ;; path from `from` to `to`". ;; ;; Floyd-Warshall over the dep graph. The 'neg' flag propagates through ;; concatenation: if any edge along the path is negative, the path's ;; flag is true. (define dl-build-reach (fn (graph nodes) (let ((reach {})) (do (for-each (fn (n) (dict-set! reach n {})) nodes) (for-each (fn (head) (when (has-key? graph head) (for-each (fn (edge) (let ((target (get edge :rel)) (n (get edge :neg))) (let ((row (get reach head))) (cond ((has-key? row target) (let ((cur (get row target))) (dict-set! row target {:any true :neg (or n (get cur :neg))}))) (else (dict-set! row target {:any true :neg n})))))) (get graph head)))) nodes) (for-each (fn (k) (for-each (fn (i) (let ((row-i (get reach i))) (when (has-key? row-i k) (let ((ik (get row-i k)) (row-k (get reach k))) (for-each (fn (j) (when (has-key? row-k j) (let ((kj (get row-k j))) (let ((combined-neg (or (get ik :neg) (get kj :neg)))) (cond ((has-key? row-i j) (let ((cur (get row-i j))) (dict-set! row-i j {:any true :neg (or combined-neg (get cur :neg))}))) (else (dict-set! row-i j {:any true :neg combined-neg}))))))) nodes))))) nodes)) nodes) reach)))) ;; Returns nil on success, or error message string on failure. (define dl-check-stratifiable (fn (db) (let ((graph (dl-build-dep-graph db)) (nodes (dl-all-relations db))) (let ((reach (dl-build-reach graph nodes)) (err nil)) (do (for-each (fn (rule) (when (nil? err) (let ((head-rel (dl-rel-name (get rule :head)))) (for-each (fn (lit) (cond ((and (dict? lit) (has-key? lit :neg)) (let ((tgt (dl-rel-name (get lit :neg)))) (when (and (not (nil? tgt)) (dl-reach-cycle? reach head-rel tgt)) (set! err (str "non-stratifiable: relation " head-rel " transitively depends through negation on " tgt " which depends back on " head-rel))))) ((dl-aggregate? lit) (let ((edge (dl-aggregate-dep-edge lit))) (when (not (nil? edge)) (let ((tgt (get edge :rel))) (when (and (not (nil? tgt)) (dl-reach-cycle? reach head-rel tgt)) (set! err (str "non-stratifiable: relation " head-rel " aggregates over " tgt " which depends back on " head-rel))))))))) (get rule :body))))) (dl-rules db)) err))))) (define dl-reach-cycle? (fn (reach a b) (and (dl-reach-row-has? reach b a) (dl-reach-row-has? reach a b)))) (define dl-reach-row-has? (fn (reach from to) (let ((row (get reach from))) (and (not (nil? row)) (has-key? row to))))) ;; Compute stratum per relation. Iteratively propagate from EDB roots. ;; Uses the per-relation max-stratum-of-deps formula. Stops when stable. (define dl-compute-strata (fn (db) (let ((graph (dl-build-dep-graph db)) (nodes (dl-all-relations db)) (strata {})) (do (for-each (fn (n) (dict-set! strata n 0)) nodes) (let ((changed true)) (do (define dl-cs-loop (fn () (when changed (do (set! changed false) (for-each (fn (head) (when (has-key? graph head) (for-each (fn (edge) (let ((tgt (get edge :rel)) (n (get edge :neg))) (let ((tgt-strat (if (has-key? strata tgt) (get strata tgt) 0)) (cur (get strata head))) (let ((needed (if n (+ tgt-strat 1) tgt-strat))) (when (> needed cur) (do (dict-set! strata head needed) (set! changed true))))))) (get graph head)))) nodes) (dl-cs-loop))))) (dl-cs-loop))) strata)))) ;; Group rules by their head's stratum. Returns dict {stratum-int -> rules}. (define dl-group-rules-by-stratum (fn (db strata) (let ((groups {}) (max-s 0)) (do (for-each (fn (rule) (let ((head-rel (dl-rel-name (get rule :head)))) (let ((s (if (has-key? strata head-rel) (get strata head-rel) 0))) (do (when (> s max-s) (set! max-s s)) (let ((sk (str s))) (do (when (not (has-key? groups sk)) (dict-set! groups sk (list))) (append! (get groups sk) rule))))))) (dl-rules db)) {:groups groups :max max-s}))))