Skip to main content

Derives catalogue

This page describes a design, not the current toolchain

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)​

DeriveWhat it generates
CloneField-by-field .clone(), following each field's clone contract
DebugDebug.fmt_debug — the {:?} formatter
DefaultDefault.default() using per-field defaults
PartialEqEq.eq — field-by-field equality
Copymarks the type as trivially copyable
EqEq.eq — field-by-field equality
HashHash.hash — field-by-field hashing
OrdOrd.cmp — lexicographic by declaration order
PartialOrdPartialOrd.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​

warning
Serialize, Deserialize, Display, Error, Builder

None 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 to A's implementation.

Reference-aware field handling​

Field types are inspected. For each field:

  • &T → the reference is copied (it is Copy by definition).
  • &checked T / &unsafe T → copied (also Copy).
  • Heap<T> → call .clone(), which allocates a new heap containing the result of T.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​

ErrorCause / 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") for Int, @debug(as = "truncated") for long Text).
  • @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) use expr instead.
  • Fields annotated @default(fn = some_fn) call some_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​

AttributeEffect
@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 at build().
  • @builder(optional) makes it optional even if its type is not Maybe<_>.
  • @builder(default = expr) supplies a default on build().
  • @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_of and friends return.
  • core.serde — Serialize / Deserialize protocol details in the standard library.
  • Diagnostics — how derive-emitted diagnostics are structured.