;; lib/datalog/api.sx — SX-data embedding API. ;; ;; Where Phase 1's `dl-program` takes a Datalog source string, ;; this module exposes a parser-free API that consumes SX data ;; directly. Two rule shapes are accepted: ;; ;; - dict: {:head :body ( ...)} ;; - list: ( <- ...) ;; — `<-` is an SX symbol used as the rule arrow. ;; ;; Examples: ;; ;; (dl-program-data ;; '((parent tom bob) (parent tom liz) (parent bob ann)) ;; '((ancestor X Y <- (parent X Y)) ;; (ancestor X Z <- (parent X Y) (ancestor Y Z)))) ;; ;; (dl-query db '(ancestor tom X)) ; same query API as before ;; ;; Variables follow the parser convention: SX symbols whose first ;; character is uppercase or `_` are variables. (define dl-rule (fn (head body) {:head head :body body})) (define dl-rule-arrow? (fn (x) (and (symbol? x) (= (symbol->string x) "<-")))) (define dl-find-arrow (fn (rl i n) (cond ((>= i n) nil) ((dl-rule-arrow? (nth rl i)) i) (else (dl-find-arrow rl (+ i 1) n))))) ;; Given a list of the form (head-elt ... <- body-lit ...) returns ;; {:head (head-elt ...) :body (body-lit ...)}. If no arrow is ;; present, the whole list is treated as the head and the body is ;; empty (i.e. a fact written rule-style). (define dl-rule-from-list (fn (rl) (let ((n (len rl))) (let ((idx (dl-find-arrow rl 0 n))) (cond ((nil? idx) {:head rl :body (list)}) (else (let ((head (slice rl 0 idx)) (body (slice rl (+ idx 1) n))) {:head head :body body}))))))) ;; Coerce a rule given as either a dict or a list-with-arrow to a dict. (define dl-coerce-rule (fn (r) (cond ((dict? r) r) ((list? r) (dl-rule-from-list r)) (else (error (str "dl-coerce-rule: expected dict or list, got " r)))))) ;; Build a db from SX data lists. (define dl-program-data (fn (facts rules) (let ((db (dl-make-db))) (do (for-each (fn (lit) (dl-add-fact! db lit)) facts) (for-each (fn (r) (dl-add-rule! db (dl-coerce-rule r))) rules) db)))) ;; Add a single fact at runtime, then re-saturate the db so derived ;; tuples reflect the change. Returns the db. (define dl-assert! (fn (db lit) (do (dl-add-fact! db lit) (dl-saturate! db) db))) ;; Remove a fact and re-saturate. Mixed relations (which have BOTH ;; user-asserted facts AND rules) are supported via :edb-keys provenance ;; — explicit facts are marked at dl-add-fact! time, the saturator uses ;; dl-add-derived! which doesn't mark them, so the retract pass can ;; safely wipe IDB-derived tuples while preserving the user's EDB. ;; ;; Effect: ;; - remove tuples matching `lit` from :facts and :edb-keys ;; - for every relation that has a rule (i.e. potentially IDB or ;; mixed), drop the IDB-derived portion (anything not in :edb-keys) ;; so the saturator can re-derive cleanly ;; - re-saturate (define dl-retract! (fn (db lit) (let ((rel-key (dl-rel-name lit))) (do ;; Drop the matching tuple from its relation list, its facts-keys, ;; its first-arg index, AND from :edb-keys (if present). (when (has-key? (get db :facts) rel-key) (let ((existing (get (get db :facts) rel-key)) (kept (list)) (kept-keys {}) (kept-index {}) (edb-rel (cond ((has-key? (get db :edb-keys) rel-key) (get (get db :edb-keys) rel-key)) (else nil))) (kept-edb {})) (do (for-each (fn (t) (when (not (dl-tuple-equal? t lit)) (do (append! kept t) (let ((tk (dl-tuple-key t))) (do (dict-set! kept-keys tk true) (when (and (not (nil? edb-rel)) (has-key? edb-rel tk)) (dict-set! kept-edb tk true)))) (when (>= (len t) 2) (let ((k (dl-arg-key (nth t 1)))) (do (when (not (has-key? kept-index k)) (dict-set! kept-index k (list))) (append! (get kept-index k) t))))))) existing) (dict-set! (get db :facts) rel-key kept) (dict-set! (get db :facts-keys) rel-key kept-keys) (dict-set! (get db :facts-index) rel-key kept-index) (when (not (nil? edb-rel)) (dict-set! (get db :edb-keys) rel-key kept-edb))))) ;; For each rule-head relation, strip the IDB-derived tuples ;; (anything not marked in :edb-keys) so the saturator can ;; cleanly re-derive without leaving stale tuples that depended ;; on the now-removed fact. (let ((rule-heads (dl-rule-head-rels db))) (for-each (fn (k) (when (has-key? (get db :facts) k) (let ((existing (get (get db :facts) k)) (kept (list)) (kept-keys {}) (kept-index {}) (edb-rel (cond ((has-key? (get db :edb-keys) k) (get (get db :edb-keys) k)) (else {})))) (do (for-each (fn (t) (let ((tk (dl-tuple-key t))) (when (has-key? edb-rel tk) (do (append! kept t) (dict-set! kept-keys tk true) (when (>= (len t) 2) (let ((kk (dl-arg-key (nth t 1)))) (do (when (not (has-key? kept-index kk)) (dict-set! kept-index kk (list))) (append! (get kept-index kk) t)))))))) existing) (dict-set! (get db :facts) k kept) (dict-set! (get db :facts-keys) k kept-keys) (dict-set! (get db :facts-index) k kept-index))))) rule-heads)) (dl-saturate! db) db)))) ;; ── Convenience: single-call source + query ─────────────────── ;; (dl-eval source query-source) parses both, builds a db, saturates, ;; runs the query, returns the substitution list. The query source ;; should be `?- goal[, goal ...].` — the parser produces a clause ;; with :query containing a list of literals which is fed straight ;; to dl-query. (define dl-eval (fn (source query-source) (let ((db (dl-program source)) (queries (dl-parse query-source))) (cond ((= (len queries) 0) (error "dl-eval: query string is empty")) ((not (has-key? (first queries) :query)) (error "dl-eval: second arg must be a `?- ...` query clause")) (else (dl-query db (get (first queries) :query))))))) ;; (dl-eval-magic source query-source) — like dl-eval but routes a ;; single-positive-literal query through `dl-magic-query` for goal- ;; directed evaluation. Multi-literal query bodies fall back to the ;; standard dl-query path (magic-sets is currently only wired for ;; single-positive goals). The caller's source is parsed afresh ;; each call so successive invocations are independent. (define dl-eval-magic (fn (source query-source) (let ((db (dl-program source)) (queries (dl-parse query-source))) (cond ((= (len queries) 0) (error "dl-eval-magic: query string is empty")) ((not (has-key? (first queries) :query)) (error "dl-eval-magic: second arg must be a `?- ...` query clause")) (else (let ((qbody (get (first queries) :query))) (cond ((and (= (len qbody) 1) (list? (first qbody)) (> (len (first qbody)) 0) (symbol? (first (first qbody)))) (dl-magic-query db (first qbody))) (else (dl-query db qbody))))))))) ;; List rules whose head's relation matches `rel-name`. Useful for ;; inspection ("show me how this relation is derived") without ;; exposing the internal `:rules` list. (define dl-rules-of (fn (db rel-name) (let ((out (list))) (do (for-each (fn (rule) (when (= (dl-rel-name (get rule :head)) rel-name) (append! out rule))) (dl-rules db)) out)))) (define dl-rule-head-rels (fn (db) (let ((seen (list))) (do (for-each (fn (rule) (let ((h (dl-rel-name (get rule :head)))) (when (and (not (nil? h)) (not (dl-member-string? h seen))) (append! seen h)))) (dl-rules db)) seen)))) ;; Wipe every relation that has at least one rule (i.e. every IDB ;; relation) — leaves EDB facts and rule definitions intact. Useful ;; before a follow-up `dl-saturate!` if you want a clean restart, or ;; for inspection of the EDB-only baseline. (define dl-clear-idb! (fn (db) (let ((rule-heads (dl-rule-head-rels db))) (do (for-each (fn (k) (do (dict-set! (get db :facts) k (list)) (dict-set! (get db :facts-keys) k {}) (dict-set! (get db :facts-index) k {}))) rule-heads) db))))