Derives catalogue
Checked, one record declared per attribute: Ord and
Default have generators; PartialOrd needs none (the standard
library's implement<T: Ord> PartialOrd for T supplies it). Every
other @derive on this page is parsed, accepted, and then reported as
not applied —
warning<W0507>: `@derive(Clone)` on `Wrap` was not applied
— no generator for this protocol yet
Two further claims below do not hold either: verum build --show-expansions is not a flag the CLI has, and there is no
@proc_macro_derive machinery anywhere in the compiler or the standard
library.
What saves most code is that records get structural behaviour
whether or not a derive ran: .clone() clones, == compares, the type
works as a Map key, and < > work through the Ord generator.
Both places the fallback did not reach are now closed — Default by a
generator, partial_cmp by the blanket — so a derive on this page that
still warns is one whose behaviour you genuinely do not get.
The attribute registry carries the measured per-derive table. Read the sections below as the intended semantics for when the generators land, not as a description of what runs today.
@derive(Name) is intended to turn a one-line annotation into a
generated implement block: six core derives — the set every
project should be able to rely on — plus a library-derive set
(Display, Error, Builder, and others) through the standard
library. This page documents, for each, what the generated code
should look like, what fields/variants it can and cannot handle, and
what diagnostics are emitted when the derive cannot proceed.
Core derives (initial release)
| Derive | What it generates |
|---|---|
Clone | Field-by-field .clone(), following each field's clone contract |
Debug | Debug.fmt_debug — the {:?} formatter |
Default | Default.default() using per-field defaults |
PartialEq | Eq.eq — field-by-field equality |
Copy | marks the type as trivially copyable |
Eq | Eq.eq — field-by-field equality |
Hash | Hash.hash — field-by-field hashing |
Ord | Ord.cmp — lexicographic by declaration order |
PartialOrd | PartialOrd.partial_cmp |
Checked against the compiler's own registration list;
Copy, Eq, Hash, Ord and PartialOrd were missing from this
table, and Serialize / Deserialize were in it and are not built in.
Derives that are named but do not exist
Serialize, Deserialize, Display, Error, BuilderNone of them is provided — not by the compiler, and not by the standard library, which declares no derive macros at all:
grep -rc '@proc_macro_derive' core/ --include='*.vr' | grep -v ':0' # nothing
The machinery for user-space derives is the intended route for these,
and the page below describes what they would generate. Applying one
today answers warning<W0507>: @derive(Name) on Type was not applied
and the type does not get the behaviour. core/cog/sign.vr carries one
such line.
Shared rules
Before the per-derive details, a few invariants every derive obeys.
Record vs sum-type dispatch
TypeInfo.kind_of<T>() returns either Record, Variant, or
Newtype. Each derive inspects it and emits the appropriate shape:
- Record (
type T is { a: A, b: B, ... }): one arm, field-by-field. - Variant (
type T is | C1(x1) | C2 { f1, f2 } | ...): one match arm per variant; generated code destructures and reconstructs. - Newtype (
type T is (A)): trivial pass-through — the derive forwards toA's implementation.
Reference-aware field handling
Field types are inspected. For each field:
&T→ the reference is copied (it isCopyby definition).&checked T/&unsafe T→ copied (alsoCopy).Heap<T>→ call.clone(), which allocates a new heap containing the result ofT.clone().Shared<T>→ call.clone(), retaining the same allocation and incrementing its strong-reference count.- Any other sized
T: Clone→ call.clone().
For PartialEq / Debug the analogous rules apply — references are
dereferenced, Heap/Shared forward to the pointee.
Generic-parameter bounds
A derive for T<A, B, ...> emits an implement block with a bound
on each generic parameter appropriate to the protocol being derived:
// @derive(Clone) on:
type Pair<A, B> is { first: A, second: B };
// generates:
implement<A: Clone, B: Clone> Clone for Pair<A, B> {
fn clone(&self) -> Self {
Self { first: self.first.clone(), second: self.second.clone() }
}
}
If a field's type is not a generic parameter, no bound is added for it (it is checked directly at the call site). This matches Rust's behaviour and minimises spurious bounds.
Opting out
A field annotated @derive_skip(Protocol) is excluded from that
protocol's generated implementation. For Default the field is
replaced by Default.default(); for Debug the field is elided
from the output; for PartialEq the field is ignored in comparison.
Misuse raises a diagnostic when the protocol cannot proceed
without the field (for example, Serialize).
Opt-in protocols vs derivable protocols
A protocol is derivable if it ships with a companion
meta fn derive_X<T>() -> TokenStream. Protocols without a derive
helper (Send, Sync, and marker protocols generally) cannot
appear in @derive(...); trying to derive them raises a diagnostic
that names the protocol and the missing derive helper.
Clone
The field-by-field algorithm below delegates copy depth to each field's
clone contract. It does not promise that the entire reachable object
graph becomes independent: references remain aliases and Shared<T>
retains identity. The language's
one-level value-copy contract
and the availability of a generated Clone implementation are separate
questions; the generator status at the top of this page still applies.
Shape
@derive(Clone)
type User is { id: Int, name: Text, email: Text };
// generates:
implement Clone for User {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
name: self.name.clone(),
email: self.email.clone(),
}
}
}
Variant handling
@derive(Clone)
type Status is | Active | Inactive { reason: Text } | Pending(Instant);
// generates:
implement Clone for Status {
fn clone(&self) -> Self {
match self {
Status.Active => Status.Active,
Status.Inactive { reason } => Status.Inactive { reason: reason.clone() },
Status.Pending(t) => Status.Pending(t.clone()),
}
}
}
Failure modes
| Error | Cause / fix |
|---|---|
| "cannot derive Clone: field f is not Clone" | add @derive(Clone) to f's type, or mark it @derive_skip(Clone) |
| "cannot derive Clone for protocol type" | derive on the concrete implementer, not the protocol |
Debug
Debug.fmt_debug(&self, f: &mut Formatter) renders the type as
TypeName { field: value, field: value } for records,
VariantName(value, value) / VariantName { field: value } for
variants, and TypeName(inner) for newtypes.
Example
@derive(Debug)
type Point is { x: Float, y: Float };
fn demo() {
let p = Point { x: 1.5, y: 2.5 };
print(&f"{p:?}"); // Point { x: 1.5, y: 2.5 }
}
Controlling output
@debug(skip)on a field omits it from the output.@debug(as = "…")on a field renders it via a named formatter (e.g.@debug(as = "hex")forInt,@debug(as = "truncated")for longText).@debug(transparent)on a newtype renders the inner value directly without the outer wrapper.
Default
Default.default() returns the zero value for primitives, the
empty value for collections, and a record or variant constructed
from per-field defaults.
Per-field defaults
- Primitives use
Default.default()recursively (0,0.0,false,"",[],{}). - Fields annotated
@default(value = expr)useexprinstead. - Fields annotated
@default(fn = some_fn)callsome_fn(). - Sum types take the first declared variant that is nullary or
whose fields are all
Default. If no such variant exists, the derive emits the corresponding diagnostic ("no default variant").
Example
@derive(Default)
type Config is {
host: Text,
port: Int,
@default(value = 30) timeout_s: Int,
@default(with = Uuid.new_v4) request_id: Uuid,
};
PartialEq
Field-by-field equality. Variants must match the same discriminant and their payloads must be equal.
@derive(PartialEq)
type Rect is { w: Float, h: Float };
assert(Rect { w: 1.0, h: 2.0 } == Rect { w: 1.0, h: 2.0 });
Controlling comparison
@eq(skip)on a field excludes it from equality.@eq(by = some_fn)compares via a named function rather than==.
PartialEq does not imply Eq — Eq (total equality) is a
marker protocol. If all fields are Eq, the compiler silently
upgrades the derivation; otherwise only PartialEq is emitted.
Display
Display.fmt(&self, f: &mut Formatter) uses a declarative template
attached via @display.
Declarative templates
@derive(Display)
@display("{name} <{email}>")
type User is { name: Text, email: Text };
The template string uses {field_name} placeholders that expand to
self.field.fmt(f) at the appropriate position. Every placeholder
must name a field of the type; unknown placeholders emit
the corresponding diagnostic ("unknown display placeholder").
Template-less derivation
For sum types a catch-all template is usually wrong. Omit the
template and provide a @display_variant("template") per variant:
@derive(Display)
type Event is
| @display_variant("connect {host}") Connect { host: Text }
| @display_variant("disconnect") Disconnect
| @display_variant("data ({bytes.len()} B)") Data { bytes: List<u8> };
Serialize and Deserialize
A pair of companion derives that implement the Serialize and
Deserialize protocols defined in core.serde. The generated code
is format-agnostic: the same implementation handles JSON, CBOR,
YAML, MessagePack, or any format that exposes a Serializer.
Record example
@derive(Serialize, Deserialize)
type User is {
id: Int,
name: Text,
email: Text,
};
Field annotations
| Attribute | Effect |
|---|---|
@serde(rename = "id") | Field name in the serialised form |
@serde(skip) | Omitted on both sides |
@serde(skip_serializing_if = …) | Condition-gated omission |
@serde(default) | Missing on deserialise → field's Default |
@serde(flatten) | Inline the field's fields into the parent |
@serde(with = module) | Use custom serialize/deserialize pair |
@serde(bound = "T: Trait") | Extra generic bound on the impl |
Sum-type tagging
Variants are tagged via the type-level @serde(tag = ..., content = ...):
@derive(Serialize, Deserialize)
@serde(tag = "kind", content = "data")
type Message is
| Ping
| Chat { author: Text, text: Text }
| Join(UserId);
produces JSON like {"kind": "Chat", "data": {"author": "Alice", "text": "hi"}}. Other tag shapes — internally_tagged,
untagged, adjacent_tag — are available; see
the core.serde section of the standard library for the full
matrix.
Error
Implements the Error protocol by delegating display to the
Display impl (which Error derives must be paired with) and
generating an optional source() method.
@derive(Debug, Display, Error)
@display("failed to connect to {host}")
type ConnectError is {
host: Text,
@error(source) cause: IoError,
};
// @error(source) marks the field returned from Error.source(); there
// may be at most one such field.
Builder
Generates a fluent builder:
@derive(Builder)
type HttpRequest is {
method: Method,
url: Url,
headers: List<Header>,
body: Maybe<Bytes>,
};
// generates:
implement HttpRequest {
pub fn builder() -> HttpRequestBuilder { HttpRequestBuilder.new() }
}
type HttpRequestBuilder is {
method: Maybe<Method>,
url: Maybe<Url>,
headers: List<Header>,
body: Maybe<Bytes>,
};
implement HttpRequestBuilder {
pub fn new() -> Self { Self { ... all None / [] ... } }
pub fn with_method(mut self, v: Method) -> Self { self.method = Maybe.Some(v); self }
pub fn with_url(mut self, v: Url) -> Self { self.url = Maybe.Some(v); self }
pub fn with_headers(mut self, v: List<Header>) -> Self { self.headers = v; self }
pub fn with_body(mut self, v: Bytes) -> Self { self.body = Maybe.Some(v); self }
pub fn build(self) -> Result<HttpRequest, BuilderError> {
Result.Ok(HttpRequest {
method: self.method.ok_or(BuilderError.Missing("method"))?,
url: self.url.ok_or(BuilderError.Missing("url"))?,
headers: self.headers,
body: self.body,
})
}
}
Customisations
@builder(required)on a field forces it to be present atbuild().@builder(optional)makes it optional even if its type is notMaybe<_>.@builder(default = expr)supplies a default onbuild().@builder(collection(add = add_header))generates.add_header(h)in addition to.with_headers([...]).@builder(rename = "uri")renames the setter (.with_uri(...)).
Composing derives
Derives compose left-to-right. @derive(Clone, Debug, PartialEq, Default) generates four separate implement blocks, each as if
derived alone. They do not interact.
A derive may depend on another: @derive(Error) requires Display
be derived or hand-written on the same type. Missing dependencies
emit the corresponding diagnostic ("derive missing dependency") with a suggestion.
Custom derives
Anyone can add a derive. Write a @proc_macro_derive(Name)
function matching the signature fn<T>() -> TokenStream using [...]
and it becomes available as @derive(Name).
See Macro kinds → derive macros for the full cookbook, and the cookbook → write a derive tutorial for a step-by-step walk-through.
See also
- Macro kinds — how derives fit among the four macro forms.
- Quote and hygiene — the mechanism derives use.
- Token-stream API — the AST types
TypeInfo.fields_ofand friends return. core.serde—Serialize/Deserializeprotocol details in the standard library.- Diagnostics — how derive-emitted diagnostics are structured.