diff --git a/lib/datalog/eval.sx b/lib/datalog/eval.sx index 677fb636..476fd0d0 100644 --- a/lib/datalog/eval.sx +++ b/lib/datalog/eval.sx @@ -362,32 +362,72 @@ ;; ── Querying ───────────────────────────────────────────────────── +;; Coerce a query argument to a list of body literals. A single literal +;; like `(p X)` (positive — head is a symbol) or `{:neg ...}` becomes +;; `((p X))`. A list of literals like `((p X) (q X))` is returned as-is. +(define + dl-query-coerce + (fn + (goal) + (cond + ((and (dict? goal) (has-key? goal :neg)) (list goal)) + ((and (list? goal) (> (len goal) 0) (symbol? (first goal))) + (list goal)) + ((list? goal) goal) + (else (error (str "dl-query: unrecognised goal shape: " goal)))))) + (define dl-query (fn (db goal) (do (dl-saturate! db) - ;; Rename anonymous '_' vars in the goal so multiple occurrences - ;; do not unify together. Keep the user-facing var list (taken - ;; before renaming) so projected results retain user names. + ;; Rename anonymous '_' vars in each goal literal so multiple + ;; occurrences do not unify together. Keep the user-facing var + ;; list (taken before renaming) so projected results retain user + ;; names. (let - ((user-vars (filter (fn (n) (not (= n "_"))) (dl-vars-of goal))) - (renamed (dl-rename-anon-lit goal (dl-make-anon-renamer)))) + ((goals (dl-query-coerce goal)) + (renamer (dl-make-anon-renamer))) (let - ((substs (dl-find-bindings (list renamed) db (dl-empty-subst))) - (results (list))) - (do - (for-each - (fn - (s) - (let - ((proj (dl-project-subst s user-vars))) + ((user-vars (dl-query-user-vars goals)) + (renamed (map (fn (g) (dl-rename-anon-lit g renamer)) goals))) + (let + ((substs (dl-find-bindings renamed db (dl-empty-subst))) + (results (list))) + (do + (for-each + (fn + (s) + (let + ((proj (dl-project-subst s user-vars))) + (when + (not (dl-tuple-member? proj results)) + (append! results proj)))) + substs) + results))))))) + +(define + dl-query-user-vars + (fn + (goals) + (let ((seen (list))) + (do + (for-each + (fn + (g) + (let + ((tgt + (if (and (dict? g) (has-key? g :neg)) (get g :neg) g))) + (for-each + (fn + (v) (when - (not (dl-tuple-member? proj results)) - (append! results proj)))) - substs) - results)))))) + (and (not (= v "_")) (not (dl-member-string? v seen))) + (append! seen v))) + (dl-vars-of tgt)))) + goals) + seen)))) (define dl-project-subst diff --git a/lib/datalog/scoreboard.json b/lib/datalog/scoreboard.json index 9fa459b9..2b5d08f2 100644 --- a/lib/datalog/scoreboard.json +++ b/lib/datalog/scoreboard.json @@ -1,8 +1,8 @@ { "lang": "datalog", - "total_passed": 165, + "total_passed": 167, "total_failed": 0, - "total": 165, + "total": 167, "suites": [ {"name":"tokenize","passed":26,"failed":0,"total":26}, {"name":"parse","passed":18,"failed":0,"total":18}, @@ -12,8 +12,8 @@ {"name":"semi_naive","passed":8,"failed":0,"total":8}, {"name":"negation","passed":10,"failed":0,"total":10}, {"name":"aggregates","passed":16,"failed":0,"total":16}, - {"name":"api","passed":9,"failed":0,"total":9}, + {"name":"api","passed":11,"failed":0,"total":11}, {"name":"demo","passed":13,"failed":0,"total":13} ], - "generated": "2026-05-08T09:12:57+00:00" + "generated": "2026-05-08T09:16:59+00:00" } diff --git a/lib/datalog/scoreboard.md b/lib/datalog/scoreboard.md index 40dd40ca..639f393a 100644 --- a/lib/datalog/scoreboard.md +++ b/lib/datalog/scoreboard.md @@ -1,6 +1,6 @@ # datalog scoreboard -**165 / 165 passing** (0 failure(s)). +**167 / 167 passing** (0 failure(s)). | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -12,5 +12,5 @@ | semi_naive | 8 | 8 | ok | | negation | 10 | 10 | ok | | aggregates | 16 | 16 | ok | -| api | 9 | 9 | ok | +| api | 11 | 11 | ok | | demo | 13 | 13 | ok | diff --git a/lib/datalog/tests/api.sx b/lib/datalog/tests/api.sx index aa127d38..577bafe9 100644 --- a/lib/datalog/tests/api.sx +++ b/lib/datalog/tests/api.sx @@ -180,6 +180,24 @@ (dl-query db (quote (p X))))) (list {:X 1} {:X 2})) + ;; Multi-goal query: pass list of literals. + (dl-api-test-set! "multi-goal query" + (dl-query + (dl-program-data + (quote ((p 1) (p 2) (p 3) (q 2) (q 3))) + (list)) + (list (quote (p X)) (quote (q X)))) + (list {:X 2} {:X 3})) + + ;; Multi-goal with comparison. + (dl-api-test-set! "multi-goal with comparison" + (dl-query + (dl-program-data + (quote ((n 1) (n 2) (n 3) (n 4) (n 5))) + (list)) + (list (quote (n X)) (list (string->symbol ">") (quote X) 2))) + (list {:X 3} {:X 4} {:X 5})) + ;; dl-rule-from-list with no arrow → fact-style. (dl-api-test-set! "no arrow → fact-like rule" (let diff --git a/plans/datalog-on-sx.md b/plans/datalog-on-sx.md index b3769e92..65bd65ac 100644 --- a/plans/datalog-on-sx.md +++ b/plans/datalog-on-sx.md @@ -222,7 +222,11 @@ large graphs. ``` - [x] `(dl-rule head body)` constructor for the dict form. - [x] `(dl-query db '(ancestor tom X))` already worked — same query API - consumes the SX-data goal. + consumes the SX-data goal. Now also accepts a *list* of body + literals for conjunctive queries: + `(dl-query db '((p X) (q X)))`, + `(dl-query db (list '(n X) '(> X 2)))`. Auto-dispatched via + `dl-query-coerce` on first-element shape. - [x] `(dl-assert! db '(parent ann pat))` → adds the fact and re-saturates. - [x] `(dl-retract! db '(parent bob ann))` → drops matching tuples from the EDB list, wipes every relation that has a rule (those are IDB), @@ -273,6 +277,14 @@ large graphs. _Newest first._ +- 2026-05-08 — `dl-query` accepts a list of body literals for + conjunctive queries, in addition to a single positive literal. + `dl-query-coerce` dispatches based on the first element's shape: + positive lit (head is a symbol) or `:neg` dict → wrap as singleton; + list of lits → use as-is. `dl-query-user-vars` collects the union + of vars across all goals (deduped, `_` filtered) for projection. + 2 new api tests: multi-goal AND, and conjunction with comparison. + - 2026-05-08 — Bug fix: `dl-check-stratifiable` now rejects recursion through aggregation (e.g., `q(N) :- count(N, X, q(X))`). The stratifier was already adding negation-like edges for aggregates,