diff --git a/lib/datalog/db.sx b/lib/datalog/db.sx index 798fcfa7..d8e4f153 100644 --- a/lib/datalog/db.sx +++ b/lib/datalog/db.sx @@ -318,6 +318,15 @@ (error (str "dl-add-rule!: expected rule dict, got " rule))) ((not (has-key? rule :head)) (error (str "dl-add-rule!: rule missing :head, got " rule))) + ((not (and (list? (get rule :head)) + (> (len (get rule :head)) 0) + (symbol? (first (get rule :head))))) + (error (str "dl-add-rule!: head must be a non-empty list " + "starting with a relation-name symbol, got " + (get rule :head)))) + ((not (list? (if (has-key? rule :body) (get rule :body) (list)))) + (error (str "dl-add-rule!: body must be a list of literals, got " + (get rule :body)))) ((dl-reserved-rel? (dl-rel-name (get rule :head))) (error (str "dl-add-rule!: '" (dl-rel-name (get rule :head)) "' is a reserved name (built-in / aggregate / negation)"))) diff --git a/lib/datalog/scoreboard.json b/lib/datalog/scoreboard.json index f36dbeba..72c71703 100644 --- a/lib/datalog/scoreboard.json +++ b/lib/datalog/scoreboard.json @@ -1,13 +1,13 @@ { "lang": "datalog", - "total_passed": 251, + "total_passed": 253, "total_failed": 0, - "total": 251, + "total": 253, "suites": [ {"name":"tokenize","passed":29,"failed":0,"total":29}, {"name":"parse","passed":22,"failed":0,"total":22}, {"name":"unify","passed":28,"failed":0,"total":28}, - {"name":"eval","passed":36,"failed":0,"total":36}, + {"name":"eval","passed":38,"failed":0,"total":38}, {"name":"builtins","passed":23,"failed":0,"total":23}, {"name":"semi_naive","passed":8,"failed":0,"total":8}, {"name":"negation","passed":10,"failed":0,"total":10}, @@ -16,5 +16,5 @@ {"name":"magic","passed":34,"failed":0,"total":34}, {"name":"demo","passed":21,"failed":0,"total":21} ], - "generated": "2026-05-10T21:05:12+00:00" + "generated": "2026-05-10T21:09:18+00:00" } diff --git a/lib/datalog/scoreboard.md b/lib/datalog/scoreboard.md index b7adc8c8..391f5d79 100644 --- a/lib/datalog/scoreboard.md +++ b/lib/datalog/scoreboard.md @@ -1,13 +1,13 @@ # datalog scoreboard -**251 / 251 passing** (0 failure(s)). +**253 / 253 passing** (0 failure(s)). | Suite | Passed | Total | Status | |-------|--------|-------|--------| | tokenize | 29 | 29 | ok | | parse | 22 | 22 | ok | | unify | 28 | 28 | ok | -| eval | 36 | 36 | ok | +| eval | 38 | 38 | ok | | builtins | 23 | 23 | ok | | semi_naive | 8 | 8 | ok | | negation | 10 | 10 | ok | diff --git a/lib/datalog/tests/eval.sx b/lib/datalog/tests/eval.sx index 75541282..e1835a0c 100644 --- a/lib/datalog/tests/eval.sx +++ b/lib/datalog/tests/eval.sx @@ -165,6 +165,22 @@ ((db (dl-program "edge(1, 2). edge(2, 3). edge(3, 1).\n reach(X, Y) :- edge(X, Y).\n reach(X, Z) :- edge(X, Y), reach(Y, Z)."))) (do (dl-saturate! db) (len (dl-relation db "reach")))) 9) + ;; Rule-shape sanity: empty-list head and non-list body raise + ;; clear errors rather than crashing inside the saturator. + (dl-et-test! "empty head rejected" + (dl-et-throws? + (fn () + (dl-add-rule! (dl-make-db) + {:head (list) :body (list)}))) + true) + + (dl-et-test! "non-list body rejected" + (dl-et-throws? + (fn () + (dl-add-rule! (dl-make-db) + {:head (list (quote p) (quote X)) :body 42}))) + true) + ;; Reserved relation names rejected as rule/fact heads. (dl-et-test! "reserved name `not` as head rejected"