Basics — trait, impl, UFCS
A trait declares a named set of method signatures. An impl T:for:U
block supplies bodies for those signatures for a concrete type U.
Declaring a trait
trait:Bumpable = {
func:bump = int32($$m Bumpable:self);
};
Inside the trait body:
- The receiver type is the trait name (
Bumpable:self). - The receiver mode is one of:
- bare (
Bumpable:self) — by-value, the impl gets a copy. $$i Bumpable:self— immutable borrow; impl cannot mutateself.$$m Bumpable:self— mutable borrow; mutations onself.fieldmust be visible to the caller.- Each method ends in
;like any other Nitpick function signature.
Writing an impl
struct:Counter = { int32:n; };
impl:Bumpable:for:Counter = {
func:bump = int32($$m Counter:self) {
self.n = self.n + 1i32;
pass self.n;
};
};
Rules:
- The impl receiver type must be the concrete type (
Counter), not the trait name. The compiler synthesises the wrapping that lets the impl participate in dynamic dispatch. - The receiver mode (
$$m/$$i/ bare) must match the trait signature. Mixing modes is a compile error. - Exactly one method per signature in the trait. Extra methods on the
type can be declared in a separate
implblock (free-standing methods, not part of any trait).
Calling a trait method (UFCS, static dispatch)
func:failsafe = int32(tbb32:err) { exit 1; };
func:main = int32() {
Counter:c = { n: 7i32 };
int32:r = c.bump(); // → 8, c.n is now 8
exit r;
};
When the receiver's type is known at compile time (here Counter),
the call resolves directly to Counter::bump — no vtable, no fat
pointer. This is the cheapest call shape and the one to prefer when
the type is statically known.
What about failsafe?
Every Nitpick program must declare failsafe — the runtime calls it
if any tbb32-tagged error propagates out of main. It is not a
trait concept, but every example in this cookbook includes it so the
fixtures compile standalone.
Worked example (bug372)
struct:Counter = { int32:n; };
trait:Bumpable = {
func:bump = int32($$m Bumpable:self);
};
impl:Bumpable:for:Counter = {
func:bump = int32($$m Counter:self) {
self.n = self.n + 1i32;
pass self.n;
};
};
func:failsafe = int32(tbb32:err) { exit 1; };
func:main = int32() {
Counter:c = { n: 5i32 };
int32:r = c.bump();
exit c.n; // exits 6
};
c.bump() is resolved statically to Counter::bump. The $$m
receiver makes mutation through self.n visible to the caller —
c.n is 6 on return.
Common diagnostics
- ARIA-043 —
dyn Treferenced but noimpl T:for:Uis in scope for any concrete typeU. See dyn surface. - ARIA-023 — Two simultaneous
$$mborrows of the same source. See borrow rules. - Receiver mode mismatch between trait and impl produces a clear message naming both the trait signature and the impl declaration.