Member semantics: two planes
Members split by how they are consumed; both planes elaborate by monomorphization — each impl
member lowers to an ordinary rule (Self ↦ target type) flowing through the standard pipeline
(classifier, stratifier, evaluability gate, reasoner) with no new evaluation machinery.
Rule plane (derive, check, query) — clause union
The trait declares the predicate; each impl contributes a type-guarded clause:
pub trait Adulthood {
derive Adult(Self)
}
impl Adulthood for USPerson { derive Adult(p: Self) :- p.age >= 18 }
impl Adulthood for GermanPerson { derive Adult(p: Self) :- p.age >= 18, p.has_residence }
elaborates to two ordinary rules with one head, Adulthood::Adult — Datalog unions same-head
rules natively, and the head-parameter guards close over <: (World assumptions (CWA / OWA)). Dispatch is derivation:
an atom Adult(p) in
pub derive CanVote(p: Person) :- Adult(p), p.registered
holds per-individual under whichever impl covers the individual’s actual type — the relational
transposition of dyn dispatch, with the no-overlap rule (Conformance) keeping the selection
single-valued per declared type (per-individual multiple classification is defined in Conformance). A
query member carries the full query signature — Self in parameter positions, a Self-free
return type (return-position Self is OE0675: it would need union types or bounded generics)
— and each impl provides a full select … from … body; the trait signature fixes the
projection shape. Query members are declared, conformance-checked, and lowered per impl, and
they surface through the same per-impl query dispatch top-level queries use — each impl’s clause
is queryable under its per-impl identity Trait::member@Target. They are not rule-body
atoms: a query-member atom in a derive/check body is refused loudly at ox check, the same
envelope discipline fn members get (Resolution) — top-level queries are not rule-body predicates
either, and the member form inherits exactly that. A single dispatch endpoint returning the
union of per-impl results is reserved. A check
member monomorphizes to ordinary RuleMode::Check rules; discharge, payload, and
delivery follow RFD 0025 unchanged. A check member’s
signature may pin its severity with the payload-arrow suffix
(check OverLimit(Self) => Severity::Error; — RFD 0026 amendment 2026-06-11, issue #230):
the pin makes blocking behavior part of the trait contract. Under a pin, an impl’s
Diagnostic { … } payload may omit severity: (it inherits the pinned one), may restate
the same severity (harmless), and is refused with OE0676 when it states a different one —
a contract whose blocking behavior varies by implementor is a weak contract. Only the severity
is pinnable: code: and message: stay per-impl, and a full Diagnostic { … } payload on the
trait-side signature is refused (OE1323). An unpinned member keeps per-impl severity freedom.
#[observe] on an impl member whose pinned severity is Error remains legal (a discharge-mode
opt-out, not a severity change).
Invocation plane (fn, mutate) — receiver resolution
A body is a single value or a single transaction; resolution is to exactly one impl. An
invocation-plane member leads with the self receiver — its first parameter must be self
(OE0675 otherwise; further Self-typed parameters are admitted after it) — because the receiver
is what dispatch selects on. A mutate member —
pub trait Closable {
mutate close(self, reason: String);
}
impl Closable for SavingsAccount {
mutate close(self, reason: String) {
update self: SavingsAccount set { status = "closed" }
}
}
— monomorphizes per impl (full RFD 0015 imperative bodies; self is the receiver variable inside
the body, Self splices to the target type) into the mutation catalog under the per-impl identity
Closable::close@SavingsAccount, callable as Closable::close: invocation dispatches on the
receiver individual’s actual committed type to the unique covering impl (<:-aware — an impl
for Account covers a SavingsAccount receiver). An ambiguous receiver — classified under two
<:-incomparable covered targets — and a receiver with no covering impl are both loud runtime
refusals naming the impls / the receiver’s types (Conformance), never an arbitrary pick. Invocation
flows through the standard mutation-dispatch surface (CLI harness / serve / SDK); the receiver is
passed as the self argument (an individual reference), exactly like any RFD 0019 entity-ref
parameter. fn members elaborate and dispatch the same way through the compute surface (Resolution has
the precise execution envelope). Mutations do not call mutations; member mutates do not change
that.