datalog: anonymous _ in negation is existential, not unbound
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 23s

The canonical Datalog idiom for "no X has any Y":

  orphan(X) :- person(X), not(parent(X, _)).

was rejected by the safety check with "negation refers to unbound
variable(s) (\"_anon1\")". The parser renames each anonymous `_`
to a fresh `_anon*` symbol so multiple `_` occurrences don't unify
with each other, and the negation safety walk then demanded all
free vars in the negated lit be bound by an earlier positive body
lit — including the renamed anonymous vars.

Anonymous vars in a negation are existentially quantified within
the negation, not requirements from outside. Added dl-non-anon-vars
to strip `_anon*` names from the `needed` set before the binding
check in dl-process-neg!. Real vars (like `X` in the orphan idiom)
still must be bound by an earlier positive body lit, just as before.

2 new regression tests (orphan idiom + multi-anon "solo" pattern);
conformance 273/273.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 08:49:20 +00:00
parent 6bae94bae1
commit 66aa003461
5 changed files with 69 additions and 9 deletions

View File

@@ -179,6 +179,27 @@
vs)
out))))
;; Filter a list of variable-name strings to exclude anonymous-renamed
;; vars (`_` in source → `_anon*` by dl-rename-anon-term). Used by
;; the negation safety check, where anonymous vars are existential
;; within the negated literal.
(define
dl-non-anon-vars
(fn
(vs)
(let
((out (list)))
(do
(for-each
(fn
(v)
(when
(not (and (>= (len v) 5)
(= (slice v 0 5) "_anon")))
(append! out v)))
vs)
out))))
(define
dl-rule-check-safety
(fn
@@ -282,8 +303,14 @@
((and (list? inner) (> (len inner) 0))
(dl-rel-name inner))
(else nil)))
(needed (dl-vars-of inner))
(missing (dl-vars-not-in (dl-vars-of inner) bound)))
;; Anonymous variables (`_` in source → `_anon*` after
;; renaming) are existentially quantified within the
;; negated literal — they don't need to be bound by
;; an earlier body lit, since `not p(X, _)` is a
;; valid idiom for "no Y exists s.t. p(X, Y)". Filter
;; them out of the safety check.
(needed (dl-non-anon-vars (dl-vars-of inner)))
(missing (dl-vars-not-in needed bound)))
(cond
((and (not (nil? inner-rn)) (dl-reserved-rel? inner-rn))
(set! err