Aggregates: counting inside a join
Area: Rules, aggregates & recursion Teaches: a
count { B }whose bodyBis a join — a relation atom together with a type-membership atom (p in Person) — so the count ranges only over partners that satisfy both. The membership filter and the relation join are the same body; reordering them must not change the count. Prerequisites: aggregates (count/exists), and first-class relations. Run:ox build examples/aggregate_membership_join && ox derive examples/aggregate_membership_join <predicate>
count { Person(p) } counts a population; count { R(a, p) } counts edges. The interesting case is the conjunction — count the R-partners of a that are also Persons — because the membership atom and the relation atom must be solved together, and a bug that drops the membership test silently under-joins to zero.
What to read in root.ar
a0 relates via R to three things, but only two of them (p1, p2) are Persons — p3 is a plain Thing:
pub fact R(a0, p1); // p1 in Person
pub fact R(a0, p2); // p2 in Person
pub fact R(a0, p3); // p3 in Thing, NOT Person
Relation-only count ranges over all three R-partners:
pub derive rel_count_is_three(a) :- Thing(a), count { R(a, p) } >= 3;
Count inside a join intersects the relation with type membership, so only the Person-typed partners count — two, not three:
pub derive member_count_at_least_two(a) :- Thing(a), count { p in Person, R(a, p) } >= 2;
pub derive member_count_at_least_three(a) :- Thing(a), count { p in Person, R(a, p) } >= 3;
Order-independence — swapping the two body atoms is the same join, so the same count of 2:
pub derive member_count_reordered_two(a) :- Thing(a), count { R(a, p), p in Person } >= 2;
Running it
ox derive <predicate> prints the head’s tuples; each firing head returns the bound a (here the individual a0):
derive(rel_count_is_three): 1 tuple(s) (a0) — count{R(a,p)} = 3, so >= 3 holds
derive(member_count_at_least_two): 1 tuple(s) (a0) — count{p in Person, R(a,p)} = 2, so >= 2 holds
derive(member_count_at_least_three): 0 tuple(s) — that count is 2, so >= 3 fails
derive(member_count_reordered_two): 1 tuple(s) (a0) — reordered body, same count of 2
The decisive contrast is rel_count_is_three vs member_count_at_least_three: both ask >= 3 of a count over a0’s R-partners, but the membership atom drops p3 (a non-Person), so the count falls from 3 to 2 and the second head does not fire. The member_count_reordered_two head proves the count is independent of atom order — both arrangements solve the same join.
Honest caveats (what runs today)
- The join inside the aggregate is the ordinary conjunctive body:
p in Personis a type-membership atom andR(a, p)is a relation atom, solved together over the shared variablep. - The count ranges over distinct solution bindings of the body, evaluated against the current state.
This example is compiled and run in CI; its join-count behaviour is pinned by a corpus test (oxc-runtime/tests/examples_corpus.rs::corpus_aggregate_membership_join_count), so the membership-inside-aggregate semantics can’t regress to the silent under-count it was written to guard against.
Source
The package’s real Argon source, transcluded from the file this corpus compiles — what you read here is exactly what CI builds.
//! #308 regression — aggregate body that mixes a TYPE-MEMBERSHIP atom
//! (`p in Person`) with a relation atom (`R(a, p)`).
//!
//! Found while writing the relation guide (executed on v0.2.0): the
//! membership-atom form inside an aggregate body silently evaluated to 0
//! for every binding, while the relation-only body was correct. Both
//! shapes must count the same pairs.
//!
//! Corpus seed: a0 relates (via R) to p1, p2, p3; only p1 and p2 are
//! Persons. So:
//!
//! * `count { R(a, p) }` over a0 = 3 (all R-partners).
//! * `count { p in Person, R(a,p) }` over a0 = 2 (the Person-typed).
//!
//! Pinned as three boolean derives whose thresholds straddle 2 vs 3.
pub type Person;
pub type Thing;
pub fact Thing(a0);
pub fact Person(p1);
pub fact Person(p2);
pub fact Thing(p3);
pub rel R(a: Thing, p: Thing);
pub fact R(a0, p1);
pub fact R(a0, p2);
pub fact R(a0, p3);
/// Relation-only baseline: count is 3, so `>= 3` fires.
pub derive rel_count_is_three(a) :- Thing(a), count { R(a, p) } >= 3;
/// Membership+relation: count is 2 (only Person-typed partners), so
/// `>= 2` fires but `>= 3` must NOT (the #308 silent-0 bug made BOTH
/// fail — the derive would be empty).
pub derive member_count_at_least_two(a) :- Thing(a), count { p in Person, R(a, p) } >= 2;
pub derive member_count_at_least_three(a) :- Thing(a), count { p in Person, R(a, p) } >= 3;
/// Order-independence: relation atom first, membership second — same 2.
pub derive member_count_reordered_two(a) :- Thing(a), count { R(a, p), p in Person } >= 2;