From d66ddc614b82eeb2847f9413cc48309a9ceea383 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 8 May 2026 09:45:15 +0000 Subject: [PATCH] datalog: aggregates work as top-level query goals (183/183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: dl-match-lit (the naive matcher used by dl-find-bindings) was missing dl-aggregate? dispatch — it was only present in dl-fbs-aux (semi-naive). Symptom: (dl-query db '(count N X (p X))) silently returned (). Two fixes: - Add aggregate branch to dl-match-lit before the positive case. - dl-query-user-vars now projects only the result var (first arg) of an aggregate goal — the aggregated var and inner-goal vars are existentials and should not leak into substitutions. 2 new aggregate tests cover count and findall as direct query goals. --- lib/datalog/eval.sx | 38 +++++++++++++++++++++++++-------- lib/datalog/scoreboard.json | 8 +++---- lib/datalog/scoreboard.md | 4 ++-- lib/datalog/tests/aggregates.sx | 15 +++++++++++++ plans/datalog-on-sx.md | 10 +++++++++ 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lib/datalog/eval.sx b/lib/datalog/eval.sx index b8ccd83f..32db2f7f 100644 --- a/lib/datalog/eval.sx +++ b/lib/datalog/eval.sx @@ -83,6 +83,7 @@ (cond ((and (dict? lit) (has-key? lit :neg)) (dl-match-negation (get lit :neg) db subst)) + ((dl-aggregate? lit) (dl-eval-aggregate lit db subst)) ((dl-builtin? lit) (let ((s (dl-eval-builtin lit subst))) @@ -426,16 +427,35 @@ (for-each (fn (g) - (let - ((tgt - (if (and (dict? g) (has-key? g :neg)) (get g :neg) g))) - (for-each - (fn - (v) + (cond + ((and (dict? g) (has-key? g :neg)) + (for-each + (fn + (v) + (when + (and (not (= v "_")) (not (dl-member-string? v seen))) + (append! seen v))) + (dl-vars-of (get g :neg)))) + ((dl-aggregate? g) + ;; Only the result var (first arg of the aggregate + ;; literal) is user-facing. The aggregated var and + ;; any vars in the inner goal are internal. + (let ((r (nth g 1))) (when - (and (not (= v "_")) (not (dl-member-string? v seen))) - (append! seen v))) - (dl-vars-of tgt)))) + (dl-var? r) + (let ((rn (symbol->string r))) + (when + (and (not (= rn "_")) + (not (dl-member-string? rn seen))) + (append! seen rn)))))) + (else + (for-each + (fn + (v) + (when + (and (not (= v "_")) (not (dl-member-string? v seen))) + (append! seen v))) + (dl-vars-of g))))) goals) seen)))) diff --git a/lib/datalog/scoreboard.json b/lib/datalog/scoreboard.json index 39c21738..b9a3fe27 100644 --- a/lib/datalog/scoreboard.json +++ b/lib/datalog/scoreboard.json @@ -1,8 +1,8 @@ { "lang": "datalog", - "total_passed": 181, + "total_passed": 183, "total_failed": 0, - "total": 181, + "total": 183, "suites": [ {"name":"tokenize","passed":26,"failed":0,"total":26}, {"name":"parse","passed":18,"failed":0,"total":18}, @@ -11,9 +11,9 @@ {"name":"builtins","passed":19,"failed":0,"total":19}, {"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":"aggregates","passed":18,"failed":0,"total":18}, {"name":"api","passed":13,"failed":0,"total":13}, {"name":"demo","passed":18,"failed":0,"total":18} ], - "generated": "2026-05-08T09:40:51+00:00" + "generated": "2026-05-08T09:45:03+00:00" } diff --git a/lib/datalog/scoreboard.md b/lib/datalog/scoreboard.md index 42982ee6..6794e91a 100644 --- a/lib/datalog/scoreboard.md +++ b/lib/datalog/scoreboard.md @@ -1,6 +1,6 @@ # datalog scoreboard -**181 / 181 passing** (0 failure(s)). +**183 / 183 passing** (0 failure(s)). | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -11,6 +11,6 @@ | builtins | 19 | 19 | ok | | semi_naive | 8 | 8 | ok | | negation | 10 | 10 | ok | -| aggregates | 16 | 16 | ok | +| aggregates | 18 | 18 | ok | | api | 13 | 13 | ok | | demo | 18 | 18 | ok | diff --git a/lib/datalog/tests/aggregates.sx b/lib/datalog/tests/aggregates.sx index dbe2f643..6392ce29 100644 --- a/lib/datalog/tests/aggregates.sx +++ b/lib/datalog/tests/aggregates.sx @@ -260,6 +260,21 @@ (list (quote low) (quote M))) (list)) + ;; Aggregates as the top-level query goal (regression for + ;; dl-match-lit aggregate dispatch and projection cleanup). + (dl-at-test-set! "count as query goal" + (dl-query + (dl-program "p(1). p(2). p(3). p(4).") + (list (quote count) (quote N) (quote X) (list (quote p) (quote X)))) + (list {:N 4})) + + (dl-at-test-set! "findall as query goal" + (dl-query + (dl-program "p(1). p(2). p(3).") + (list (quote findall) (quote L) (quote X) + (list (quote p) (quote X)))) + (list {:L (list 1 2 3)})) + (dl-at-test-set! "distinct counted once" (dl-query (dl-program diff --git a/plans/datalog-on-sx.md b/plans/datalog-on-sx.md index 04e2948c..bfd1f256 100644 --- a/plans/datalog-on-sx.md +++ b/plans/datalog-on-sx.md @@ -284,6 +284,16 @@ large graphs. _Newest first._ +- 2026-05-08 — Bug fix: aggregates work as top-level query goals. + `dl-match-lit` (the naive matcher used by `dl-find-bindings`) was + missing the `dl-aggregate?` dispatch — it was only present in + `dl-fbs-aux` (semi-naive). Symptom: `(dl-query db '(count N X (p X)))` + silently returned `()`. Also updated `dl-query-user-vars` to project + only the result var (first arg) of an aggregate goal — the + aggregated var and inner-goal vars are existentials and should not + appear in the projected substitution. 2 new aggregate tests cover + the regression. + - 2026-05-08 — Convenience: `dl-eval source query-source`. Parses both strings, builds a db, saturates, runs the query, returns the substitution list. Single-call user-friendly entry. 2 new