Repository navigation
Description
A rule that calls a rule with not / not-join in its body returns too few rows, often none. Calling the negated rule from the query works:
(transact [[:m1 :msg/actor :a1]
[:r1 :run/actor :a1] [:r1 :run/to :s1]
[:r2 :run/actor :a1] [:r2 :run/to :s2] [:r2 :run/done true]])
(rule [(cur ?a ?s) [?r :run/actor ?a] [?r :run/to ?s] (not-join [?r] [?r :run/done true])])
(rule [(st ?m ?s) [?m :msg/actor ?a] (cur ?a ?s)])
(query [:find ?m ?s :where [?m :msg/actor ?a] (cur ?a ?s)])
;; => :m1 :s1
(query [:find ?m ?s :where (st ?m ?s)])
;; expected: :m1 :s1
;; actual: No results found.
The same happens with not in place of not-join, with a literal argument ((st :m1 ?s)), and with a rule that only forwards the call ((rule [(st3 ?a ?s) (cur ?a ?s)])). A rule calling a rule without negation is not affected.
Reproduces on v2.0.7 and on v3 (c501f549), in the REPL.
Cause
StratifiedEvaluator::evaluate (src/query/datalog/evaluator.rs) gives a positive dependency the same stratum as the rule it depends on, so st and cur share stratum 0. Within a stratum it then:
- runs the positive-only rules to a fixpoint (
sthere), then - runs each mixed rule (
not,not-join,or,or-join) once, inall_predsorder (curhere).
So st is evaluated before any cur fact exists. When the caller is a mixed rule too, the order of all_preds decides: callers are listed before the rules they call, so the caller still runs first. Asking for the callee first in the query changes the answer:
(rule [(stm ?m ?s) [?m :msg/actor ?a] (cur ?a ?s) (not-join [?m] [?m :nope true])])
(query [:find ?m ?s :where (stm ?m ?s)]) ;; => No results found.
(query [:find ?m ?s :where (cur ?a ?x) (stm ?m ?s)]) ;; => :m1 :s1
The result is silently wrong, with no error.
Possible fix
Evaluate rules within a stratum in dependency order, so a rule runs only after every mixed rule it calls has finished. One way: in stratify(), treat a positive dependency on a mixed predicate like a negative one (dep_stratum + 1), so the callee always completes in a lower stratum. That also covers a mixed rule calling a mixed rule. Recursion through a mixed rule needs a separate look, since each mixed rule runs only once per stratum.
Found while
Writing the actor state use case for the docs (project-minigraf/minigraf-docs#36). The docs example uses a query in place of the rule to avoid this.
Generated by Claude Code
Activity
test(stratify): caller of a recursive mixed rule (#628)
Merge pull request #631 from project-minigraf/fix/628-mixed-rule-stratum
fix(rules): stratify a rule above a not/or rule it calls (#628) (#634)
adityamukho commentedon Oct 19, 2026
Fixed on
v3in #631 (224c290) and onmainin #634, for the next v2.0.x patch. The fix puts a rule in a stratum above any rule withnot/not-join/or/or-jointhat it calls, unless the two call each other. Recursion through such a rule is still broken and is tracked in #633. This issue stays open until the v2.x patch is released (support policy).release: v2.0.8
adityamukho commentedon Oct 20, 2026
Released in v2.0.8 (crates.io 2.0.8, 2026-10-20); also fixed on
v3for v3.0.0. Moved to "Fixed in v2.0.8" in #421.