datalog: built-ins + body arithmetic + order-aware safety (Phase 4, 106/106)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 50s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 50s
New lib/datalog/builtins.sx: (< <= > >= = !=) and (is X expr) with + - * /. dl-eval-arith recursively evaluates nested compounds. Safety analysis now walks body left-to-right tracking the bound set: comparisons require all args bound, is RHS vars must be bound (LHS becomes bound), = special-cases the var/non-var combos. db.sx keeps the simple safety check as a forward-reference fallback; builtins.sx redefines dl-rule-check-safety to the comprehensive version. eval.sx dispatches built-ins through dl-eval-builtin instead of erroring. 19 new tests.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
;; lib/datalog/db.sx — Datalog database (EDB + IDB + rules) + safety.
|
||||
;; lib/datalog/db.sx — Datalog database (EDB + IDB + rules) + safety hook.
|
||||
;;
|
||||
;; A db is a mutable dict:
|
||||
;; {:facts {<rel-name-string> -> (literal ...)}
|
||||
@@ -8,13 +8,12 @@
|
||||
;; directly against rule body literals. Each relation's tuple list is
|
||||
;; deduplicated on insert.
|
||||
;;
|
||||
;; Phase 3 makes no distinction between EDB (asserted) and IDB (derived);
|
||||
;; a future phase may track provenance for retraction.
|
||||
;; Phase 3 introduced safety analysis for head variables; Phase 4 (in
|
||||
;; lib/datalog/builtins.sx) swaps in the real `dl-rule-check-safety`,
|
||||
;; which is order-aware and understands built-in predicates.
|
||||
|
||||
(define dl-make-db (fn () {:facts {} :rules (list)}))
|
||||
|
||||
;; The relation name (as string) of a positive literal, or of the
|
||||
;; underlying literal of a negative one. nil for malformed input.
|
||||
(define
|
||||
dl-rel-name
|
||||
(fn
|
||||
@@ -59,8 +58,6 @@
|
||||
((and (list? lit) (> (len lit) 0)) true)
|
||||
(else false))))
|
||||
|
||||
;; Membership using dl-deep tuple equality (handles var/constant symbols
|
||||
;; and numbers consistently).
|
||||
(define
|
||||
dl-tuple-equal?
|
||||
(fn
|
||||
@@ -89,7 +86,6 @@
|
||||
((dl-tuple-equal? lit (first lits)) true)
|
||||
(else (dl-tuple-member? lit (rest lits))))))
|
||||
|
||||
;; Ensure :facts has a list for the given relation key, then return it.
|
||||
(define
|
||||
dl-ensure-rel!
|
||||
(fn
|
||||
@@ -102,8 +98,6 @@
|
||||
(dict-set! facts rel-key (list)))
|
||||
(get facts rel-key)))))
|
||||
|
||||
;; All tuples currently known for a relation (EDB ∪ IDB). Returns empty
|
||||
;; list when relation hasn't been seen.
|
||||
(define
|
||||
dl-rel-tuples
|
||||
(fn
|
||||
@@ -112,7 +106,6 @@
|
||||
((facts (get db :facts)))
|
||||
(if (has-key? facts rel-key) (get facts rel-key) (list)))))
|
||||
|
||||
;; Add a ground literal. Returns true iff the literal was new.
|
||||
(define
|
||||
dl-add-fact!
|
||||
(fn
|
||||
@@ -131,72 +124,53 @@
|
||||
((dl-tuple-member? lit tuples) false)
|
||||
(else (do (append! tuples lit) true)))))))))
|
||||
|
||||
;; Collect variables appearing in the positive body literals of `body`.
|
||||
;; The full safety check lives in builtins.sx (it has to know which
|
||||
;; predicates are built-ins). dl-add-rule! calls it via forward
|
||||
;; reference; load builtins.sx alongside db.sx in any setup that
|
||||
;; adds rules. The fallback below is used if builtins.sx isn't loaded.
|
||||
(define
|
||||
dl-positive-body-vars
|
||||
(fn
|
||||
(body)
|
||||
(let
|
||||
((vars (list)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
(lit)
|
||||
(when
|
||||
(dl-positive-lit? lit)
|
||||
(for-each
|
||||
(fn
|
||||
(v)
|
||||
(when (not (dl-member-string? v vars)) (append! vars v)))
|
||||
(dl-vars-of lit))))
|
||||
body)
|
||||
vars))))
|
||||
|
||||
;; Collect variables in any literal (positive, negated, or built-in).
|
||||
(define
|
||||
dl-all-body-vars
|
||||
(fn
|
||||
(body)
|
||||
(let
|
||||
((vars (list)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
(lit)
|
||||
(let
|
||||
((target (if (and (dict? lit) (has-key? lit :neg)) (get lit :neg) lit)))
|
||||
(for-each
|
||||
(fn
|
||||
(v)
|
||||
(when (not (dl-member-string? v vars)) (append! vars v)))
|
||||
(dl-vars-of target))))
|
||||
body)
|
||||
vars))))
|
||||
|
||||
;; Return the list of head variables NOT covered by some positive body
|
||||
;; literal. A safe rule has an empty list. The check ignores '_' since
|
||||
;; that is treated as a fresh anonymous variable per occurrence.
|
||||
(define
|
||||
dl-rule-unsafe-head-vars
|
||||
dl-rule-check-safety
|
||||
(fn
|
||||
(rule)
|
||||
(let
|
||||
((head (get rule :head))
|
||||
(body (get rule :body))
|
||||
(head-vars (dl-vars-of head))
|
||||
(body-vars (dl-positive-body-vars body))
|
||||
(out (list)))
|
||||
((head-vars (dl-vars-of (get rule :head))) (body-vars (list)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
(v)
|
||||
(lit)
|
||||
(when
|
||||
(and (not (dl-member-string? v body-vars)) (not (= v "_")))
|
||||
(append! out v)))
|
||||
head-vars)
|
||||
out))))
|
||||
(and
|
||||
(list? lit)
|
||||
(> (len lit) 0)
|
||||
(not (and (dict? lit) (has-key? lit :neg))))
|
||||
(for-each
|
||||
(fn
|
||||
(v)
|
||||
(when
|
||||
(not (dl-member-string? v body-vars))
|
||||
(append! body-vars v)))
|
||||
(dl-vars-of lit))))
|
||||
(get rule :body))
|
||||
(let
|
||||
((missing (list)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
(v)
|
||||
(when
|
||||
(and
|
||||
(not (dl-member-string? v body-vars))
|
||||
(not (= v "_")))
|
||||
(append! missing v)))
|
||||
head-vars)
|
||||
(cond
|
||||
((> (len missing) 0)
|
||||
(str
|
||||
"head variable(s) "
|
||||
missing
|
||||
" do not appear in any body literal"))
|
||||
(else nil))))))))
|
||||
|
||||
;; Add a rule. Rejects unsafe rules with a pointer at the offending var.
|
||||
(define
|
||||
dl-add-rule!
|
||||
(fn
|
||||
@@ -208,21 +182,14 @@
|
||||
(error (str "dl-add-rule!: rule missing :head, got " rule)))
|
||||
(else
|
||||
(let
|
||||
((unsafe (dl-rule-unsafe-head-vars rule)))
|
||||
((err (dl-rule-check-safety rule)))
|
||||
(cond
|
||||
((> (len unsafe) 0)
|
||||
(error
|
||||
(str
|
||||
"dl-add-rule!: unsafe rule — head variable(s) "
|
||||
unsafe
|
||||
" do not appear in any positive body literal of "
|
||||
rule)))
|
||||
((not (nil? err)) (error (str "dl-add-rule!: " err)))
|
||||
(else
|
||||
(let
|
||||
((rules (get db :rules)))
|
||||
(do (append! rules rule) true)))))))))
|
||||
|
||||
;; Load a parsed clause (fact or rule) into the db. Queries are ignored.
|
||||
(define
|
||||
dl-add-clause!
|
||||
(fn
|
||||
@@ -233,7 +200,6 @@
|
||||
(dl-add-fact! db (get clause :head)))
|
||||
(else (dl-add-rule! db clause)))))
|
||||
|
||||
;; Parse source text and load every clause into the db. Returns the db.
|
||||
(define
|
||||
dl-load-program!
|
||||
(fn
|
||||
@@ -242,14 +208,12 @@
|
||||
((clauses (dl-parse source)))
|
||||
(do (for-each (fn (c) (dl-add-clause! db c)) clauses) db))))
|
||||
|
||||
;; Convenience: build a db from source in one step.
|
||||
(define
|
||||
dl-program
|
||||
(fn (source) (let ((db (dl-make-db))) (dl-load-program! db source))))
|
||||
|
||||
(define dl-rules (fn (db) (get db :rules)))
|
||||
|
||||
;; Total number of stored ground tuples across all relations.
|
||||
(define
|
||||
dl-fact-count
|
||||
(fn
|
||||
|
||||
Reference in New Issue
Block a user