datalog: reject malformed dict body literals
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m26s

A dict in a rule body that isn't `{:neg <positive-lit>}` (the only
recognised dict shape) used to silently fall through every dispatch
clause in dl-rule-check-safety, contributing zero bound variables.
The user would then see a confusing "head variable(s) X do not
appear in any positive body literal" pointing at the head — not at
the actual bug in the body. Typos like `{:negs ...}` are the typical
trigger.

dl-process-lit! now flags both:

  - a dict that lacks :neg
  - a bare number / string / symbol used as a body lit

with a clear error naming the offending literal.

1 new regression test; conformance 265/265.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 08:04:03 +00:00
parent 00881f84eb
commit ba60db2eef
5 changed files with 50 additions and 8 deletions

View File

@@ -301,6 +301,18 @@
(cond
((and (dict? lit) (has-key? lit :neg))
(dl-process-neg! lit))
;; A bare dict that is not a recognised negation is
;; almost certainly a typo (e.g. `{:negs ...}` instead
;; of `{:neg ...}`). Without this guard the dict would
;; silently fall through every clause; the head safety
;; check would then flag the head variables as unbound
;; even though the real bug is the malformed body lit.
((dict? lit)
(set! err
(str "body literal is a dict but lacks :neg — "
"the only dict-shaped body lit recognised is "
"{:neg <positive-lit>} for stratified "
"negation, got " lit)))
((dl-aggregate? lit) (dl-process-agg! lit))
((dl-eq? lit) (dl-process-eq! lit))
((dl-is? lit) (dl-process-is! lit))
@@ -315,7 +327,15 @@
"syntax; nested `not(...)` is not supported "
"(use stratified negation via an "
"intermediate relation)")))
(else (dl-add-bound! (dl-vars-of lit))))))))))
(else (dl-add-bound! (dl-vars-of lit))))))
(else
;; Anything that's not a dict, not a list, or an
;; empty list. Numbers / strings / symbols as body
;; lits don't make sense — surface the type.
(set! err
(str "body literal must be a positive lit, "
"built-in, aggregate, or {:neg ...} dict, "
"got " lit)))))))
(for-each dl-process-lit! body)
(when
(nil? err)