Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Aggregates: counting inside a join

Area: Rules, aggregates & recursion Teaches: a count { B } whose body B is 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 Person is a type-membership atom and R(a, p) is a relation atom, solved together over the shared variable p.
  • 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;