Expand description
quickcheck-richderive
A #[derive(Arbitrary)] proc-macro that emits a native
quickcheck::Arbitrary implementation — both arbitrary and a real
shrink — for structs and enums.
§Introduction
Unlike bridges that route generation through arbitrary::Unstructured byte
buffers, this derive produces a genuine quickcheck impl that calls
Arbitrary::arbitrary / Arbitrary::shrink directly on the fields, so
quickcheck’s size control and shrinking work as intended.
This crate does not depend on quickcheck itself (only as a dev-dependency
for its own tests). The generated code refers to your quickcheck via the
crate attribute, defaulting to ::quickcheck, so
consumers bring their own.
§Usage
use quickcheck_richderive::Arbitrary;
#[derive(Clone, Debug, Arbitrary)]
struct Point {
x: i32,
y: i32,
}The generated impl is wrapped in an anonymous const _: () = { … }; for hygiene
and provides:
fn arbitrary(g: &mut Gen) -> Self;
fn shrink(&self) -> Box<dyn Iterator<Item = Self>>;arbitrary builds each field with quickcheck::Arbitrary::arbitrary; shrink
shrinks one field at a time, holding the others at their current value, and
chains the resulting iterators. The derived type must be Clone (a
quickcheck::Arbitrary supertrait) — shrink clones self to hold the
unchanged fields.
§Attribute surface
All attributes live under the quickcheck path: #[quickcheck(...)]. They apply
at three positions — the container (the struct/enum), each field, and
each enum variant.
§Container attributes (on the struct / enum)
| Attribute | Meaning |
|---|---|
crate = "..." | Base path for the emitted Arbitrary / Gen. Default ::quickcheck. |
bound = "..." | Repeatable. Replaces the inferred generic bounds. |
with = "mod" | A module exporting both mod::arbitrary and mod::shrink — overrides both halves at once. Serde-style. |
arbitrary = "fn" | Generate the whole value via this function. |
shrink = "fn" | Shrink the whole value via this function. |
box = "..." | Override the Box type used in the shrink return. |
§crate = "path::to::quickcheck"
Point the generated code at a re-exported or renamed quickcheck. Useful when
quickcheck is re-exported through another crate, or vendored under a different
name.
use quickcheck_richderive::Arbitrary;
// `quickcheck` re-exported under a different path:
mod reexport {
pub use quickcheck::*;
}
#[derive(Clone, Arbitrary)]
#[quickcheck(crate = "reexport")]
struct S {
x: u8,
}§bound = "P: Bound, Q: Other" (repeatable)
By default the derive infers a <FieldTy>: quickcheck::Arbitrary bound for each
generated field whose type mentions a generic parameter (type or const) — e.g.
T: Arbitrary for a T field, Vec<T>: Arbitrary, <T as Trait>::Item: Arbitrary for a projection, or Only<N>: Arbitrary for a const-generic field
type. Fields produced via with / default, and skip / variant-with
variants, contribute no bound (they are never generated via Arbitrary).
Bounding the field types — rather than the params inside them — keeps projected /
associated types sound. It additionally adds a single Self: Clone + 'static
bound — the exact Arbitrary: Clone + 'static supertrait obligation on the
implementing type, which a <FieldTy>: Arbitrary bound (e.g. Vec<T>: Arbitrary)
does not imply. Bounding Self rather than each T avoids over-constraining
manually-Clone types and correctly handles lifetime-generic targets. If you
supply one or more bound attributes, they replace that inference entirely:
the generated where clause becomes the type’s own predicates plus exactly
the predicates you list (multiple bound = "..." accumulate).
// Default inference: `where T: quickcheck::Arbitrary`.
#[derive(Clone, Arbitrary)]
struct Wrapper<T>(T);
// Custom bounds replace the inference entirely.
#[derive(Clone, Arbitrary)]
#[quickcheck(bound = "T: Clone + Default + 'static")]
struct Defaulted<T> {
#[quickcheck(default)]
inner: T,
}
'static/Clonecaveat. Becausequickcheck::Arbitrary: Clone + 'static, the impl always needs those for the type itself. When you override withbound, you are responsible for any bounds the body relies on — a generic param that is still generated viaArbitrary::arbitrarymust keep: quickcheck::Arbitrary, and a param used in ashrink/Clonecontext must keep: Clone + 'static. The inference is dropped wholesale, so an incomplete custom bound will fail to compile.
§Container with / arbitrary / shrink
Override generation and/or shrinking of the whole value. Three knobs,
mirroring serde’s serialize_with / deserialize_with / with triad:
| Attribute | Value | Signature(s) the consumer must export |
|---|---|---|
with = "mod" | a module | fn arbitrary(g: &mut Gen) -> Self and fn shrink(v: &Self) -> Box<dyn Iterator<Item = Self>> |
arbitrary = "fn" | a function | fn(g: &mut Gen) -> Self |
shrink = "fn" | a function | fn(v: &Self) -> Box<dyn Iterator<Item = Self>> |
with bundles both halves through one module; arbitrary and shrink are the
per-direction overrides. with is mutually exclusive with both arbitrary
and shrink (compile error). Defaults that kick in when an attribute is absent:
with = "mod"alone: both halves come from the module.arbitrary = "fn"alone: gen usesfn; shrink is empty (no shrink route).shrink = "fn"alone: gen is still field/variant-derived; shrink usesfn.arbitrary = "fn"+shrink = "fn": each direction from its own function.
§with = "mod" — module pair (serde-style)
#[derive(Clone, Arbitrary)]
#[quickcheck(with = "geo_helpers")]
struct GeoLocation { /* private, range-checked fields */ }
mod geo_helpers {
use super::GeoLocation;
use quickcheck::Gen;
pub fn arbitrary(g: &mut Gen) -> GeoLocation {
let lat = (i64::arbitrary(g) % 9_001) as f64 / 100.0; // [-90, 90]
let lon = (i64::arbitrary(g) % 18_001) as f64 / 100.0; // [-180, 180]
GeoLocation::try_new(lat, lon, None).unwrap()
}
pub fn shrink(v: &GeoLocation) -> Box<dyn Iterator<Item = GeoLocation>> {
// …whatever shrink strategy makes sense for the validated invariants
Box::new(std::iter::empty())
}
}§arbitrary = "fn" — single-fn gen, no shrink
When the type has no useful shrink and you only need to override generation:
#[derive(Clone, Arbitrary)]
#[quickcheck(arbitrary = "gen_geo")]
struct GeoLocation { /* private, range-checked fields */ }
fn gen_geo(g: &mut Gen) -> GeoLocation {
let lat = (i64::arbitrary(g) % 9_001) as f64 / 100.0;
let lon = (i64::arbitrary(g) % 18_001) as f64 / 100.0;
GeoLocation::try_new(lat, lon, None).unwrap()
}§shrink = "fn" — single-fn shrink, field-derived gen
Useful when the field-by-field default gen is fine but you want a smarter shrink strategy.
§box = "path::to::Box"
Override the Box type in the generated shrink return
(shrink(&self) -> Box<dyn Iterator<Item = Self>>). By default it is
::std::boxed::Box with the std feature, or an internally-aliased
alloc::boxed::Box in no-std (see Features); box overrides either,
e.g. to point at a re-exported / custom Box:
#[derive(Clone, Arbitrary)]
#[quickcheck(box = "my_crate::reexport::Box")]
struct S { x: u32 }The
boxpath is emitted verbatim — the consuming crate must be able to resolve it. In particularbox = "alloc::boxed::Box"(or"::alloc::...") requires the consumer’s ownextern crate alloc;, since the macro cannot add a crate-root import. For no-std theallocfeature is the self-contained choice (it aliasesallocinternally); reach forboxonly for a genuinely customBox.
§Field attributes (struct fields, and fields of struct/tuple variants)
| Attribute | Value | Effect |
|---|---|---|
with = "mod" | a module | mod::arbitrary(g: &mut Gen) -> FieldT + mod::shrink(v: &FieldT) -> Box<dyn Iterator<Item = FieldT>> |
arbitrary = "fn" | a function | fn(g: &mut Gen) -> FieldT — gen half only |
shrink = "fn" | a function | fn(v: &FieldT) -> Box<dyn Iterator<Item = FieldT>> — shrink half only |
default | — | Generate via Default::default(); the field is held constant when shrinking. |
with is mutually exclusive with arbitrary and shrink. default is mutually
exclusive with with and arbitrary. The per-field shrink rule:
with = "mod"→ usemod::shrink;shrink = "fn"→ usefn;- plain field →
quickcheck::Arbitrary::shrink; arbitrary = "fn"-without-shrink, ordefault→ held constant (never shrunk).
#[derive(Clone, Arbitrary)]
struct Packet {
// serde-style pair: one module provides both halves
#[quickcheck(with = "foreign_id_helpers")]
id: ForeignId,
// single-fn forms — useful when you only need one direction
#[quickcheck(arbitrary = "gen_other", shrink = "shrink_other")]
other: ForeignOther,
// never generated from `g`; always `Default::default()`, never shrunk
#[quickcheck(default)]
cached: Cache,
// plain: Arbitrary::arbitrary / Arbitrary::shrink
payload: Vec<u8>,
}
mod foreign_id_helpers {
use quickcheck::Gen;
use super::ForeignId;
pub fn arbitrary(g: &mut Gen) -> ForeignId { ForeignId::new(u32::arbitrary(g)) }
pub fn shrink(v: &ForeignId) -> Box<dyn Iterator<Item = ForeignId>> {
Box::new(v.as_u32().shrink().map(ForeignId::new))
}
}§Variant attributes (enum variants)
| Attribute | Value | Effect |
|---|---|---|
skip | — | Exclude from arbitrary selection. A value that is this variant shrinks to empty. If every variant is skip, a compile_error! is produced. |
with = "mod" | a module | mod::arbitrary(g: &mut Gen) -> Self (yielding this variant) + mod::shrink(v: &Self) -> Box<dyn Iterator<Item = Self>> |
arbitrary = "fn" | a function | Generate the whole Self value as this variant: fn(g: &mut Gen) -> Self. Takes precedence over the variant’s field attributes. |
shrink = "fn" | a function | Shrink a value of this variant: fn(v: &Self) -> Box<dyn Iterator<Item = Self>>. arbitrary-without-shrink ⇒ empty for that variant. |
arbitrary picks uniformly among the non-skipped variants via g.choose.
Variants without a variant-level with or arbitrary are generated
field-by-field; their fields accept the field attributes above. The same
mutual-exclusion rules as the container apply (with vs arbitrary/shrink).
#[derive(Clone, Arbitrary)]
enum Event {
#[quickcheck(skip)]
Internal,
Tick,
Resize { width: u32, height: u32 },
// serde-style pair on a variant
#[quickcheck(with = "custom_helpers")]
Custom(Payload),
// single-fn forms
#[quickcheck(arbitrary = "gen_special")]
Special(u32),
// per-field attributes inside a variant
Frame {
#[quickcheck(arbitrary = "gen_pixels")]
pixels: Pixels,
index: u64,
},
}
mod custom_helpers {
use super::*;
pub fn arbitrary(g: &mut Gen) -> Event { Event::Custom(Payload::arbitrary(g)) }
pub fn shrink(_v: &Event) -> Box<dyn Iterator<Item = Event>> { Box::new(std::iter::empty()) }
}
fn gen_special(g: &mut Gen) -> Event { Event::Special(u32::arbitrary(g)) }
fn gen_pixels(g: &mut Gen) -> Pixels { Pixels::arbitrary(g) }§Codegen summary
- Output — wrapped in
const _: () = { impl … { fn arbitrary; fn shrink } };;Arbitrary/Genare referenced through thecratepath. whereclause —split_for_impl+ either explicitbounds or inferred predicates:Self: Clone + 'static(theArbitrarysupertrait) plus a<FieldTy>: <crate>::Arbitraryper generated field type that mentions a generic (type or const) param.- Struct —
arbitrarybuilds the struct literal (each field per its rule);shrinkclonesself, assigns one shrunk field at a time, and chains. - Enum —
arbitrarydoesmatch *g.choose(&[non-skipped indices]).unwrap();shrinkmatches the current variant and rebuilds it explicitly with one field shrunk and the rest cloned. Unit variants,skipped variants, and fully held-constant variants shrink to empty.
§#[quickcheck] attribute
In addition to the derive, the crate ships a #[quickcheck_richderive::quickcheck]
proc-macro-attribute — a drop-in alternative to
quickcheck_macros::quickcheck that adds:
- Per-arg generator strategies via
#[strategy(path)]on the fn parameters (proptest-style shape): give individual fn arguments their ownfn(&mut Gen) -> Tinstead of the type’sArbitraryimpl. - Per-test runner config: tune
cases,max_tests,gen_size, andmin_tests_passedat the call site. crate = "..."knob: point the generated code at a re-exported or renamedquickcheck(mirrors the derive’scrateattribute).quickcheck_assert!/quickcheck_assert_eq!/quickcheck_assert_ne!— assertion macros that returnTestResult::error(...)on failure (no panic), so the runner can shrink without losing the formatted failure message.
The bare form behaves like vanilla #[quickcheck] from quickcheck_macros —
each arg uses its type’s Arbitrary:
use quickcheck_richderive::quickcheck;
#[quickcheck]
fn idempotent(xs: Vec<u32>) -> bool {
let mut a = xs.clone();
a.sort(); a.sort();
let mut b = xs;
b.sort();
a == b
}With a per-arg strategy and runner config:
use quickcheck_richderive::quickcheck;
fn small_positive(g: &mut ::quickcheck::Gen) -> i32 {
(u8::arbitrary(g) as i32) + 1
}
#[quickcheck(cases = 1000, gen_size = 64)]
fn round_trip(
#[strategy(small_positive)] a: i32,
b: String,
) -> bool {
let _ = (a, b);
true
}§Attribute keys (outer #[quickcheck(...)])
| Key | Type | Default | Effect |
|---|---|---|---|
cases = N | u64 literal | 100 | .tests(N) on the runner |
max_tests = N | u64 literal | 10_000 | .max_tests(N) (discard cap) |
gen_size = N | usize literal | 100 | Gen::new(N) |
min_tests_passed = N | u64 literal | unset | .min_tests_passed(N) (omits the chained call when unset) |
crate = "..." | path string | ::quickcheck | base path for Arbitrary / Gen / QuickCheck / TestResult and the injected quickcheck_assert! family |
Any other key is rejected with a focused error pointed at the key span — in
particular, per-arg generators are no longer declared in the outer
attribute. Use #[strategy(...)] on the parameter instead.
No seed key. quickcheck::Gen has no public seed API in 1.x, so a
seed = … knob would require an unsafe transmute or a fork of quickcheck
itself. If you need deterministic sequences, build a custom generator backed
by an RNG you control (e.g. rand::rngs::StdRng::seed_from_u64) and wire it
through a #[strategy(...)] parameter attribute; the macro provides no
shortcut.
§#[strategy(path)] on fn parameters
Per-argument generator overrides live on the fn parameters as a proptest- style sibling attribute:
fn small_positive(g: &mut ::quickcheck::Gen) -> i32 {
(u32::arbitrary(g) % 100) as i32 + 1
}
#[quickcheck_richderive::quickcheck(cases = 1000)]
fn round_trip(
#[strategy(small_positive)] a: i32,
b: String, // no override — uses <String as Arbitrary>::arbitrary
) -> bool {
let _ = (a, b);
true
}The body of #[strategy(...)] is a path expression (no quoting). The
path must resolve to a fn(&mut Gen) -> ArgT — same signature as
Arbitrary::arbitrary. Each strategy-bearing arg is wrapped in a private
newtype whose Arbitrary::arbitrary calls your path and whose shrink
delegates to <ArgT as Arbitrary>::shrink — so shrinking still works on
counter-examples even though you only supplied a generator. No Shrink
knob is exposed on the attribute surface.
#[strategy(...)] is only valid on plain ident: type parameters — a
destructuring pattern ((a, b): (T, U)) is rejected by the same
diagnostic that already rejects pattern-bound parameters.
§quickcheck_assert! / quickcheck_assert_eq! / quickcheck_assert_ne!
The attribute injects three assertion macros into scope of the user’s body.
On failure they return TestResult::error(...) with a formatted message
that includes file/line + the stringified condition (or left/right
debug-printed for the eq / ne forms) + an optional user-supplied
format!-style suffix.
use quickcheck_richderive::quickcheck;
use quickcheck::TestResult;
#[quickcheck]
fn sort_is_sorted(xs: Vec<u32>) -> TestResult {
let mut sorted = xs.clone();
sorted.sort();
for w in sorted.windows(2) {
quickcheck_assert!(w[0] <= w[1], "ordering violated: {:?}", w);
}
TestResult::passed()
}
#[quickcheck]
fn double_then_halve(x: u32) -> TestResult {
quickcheck_assert_eq!(x.wrapping_mul(2).wrapping_div(2), x);
TestResult::passed()
}Failures bubble up as TestResult::error rather than as panics, which keeps
the runner’s shrinker active and lets the formatted message reach
quickcheck’s diagnostic output. Users who want plain assert! keep using
it — panic!-based failures still trigger shrinking (just with noisier
output). The macros are intended for TestResult-returning tests; with a
-> bool or -> () body the return would be a type error.
§crate = "::path::to::quickcheck"
Point the generated code at a re-exported or renamed quickcheck. Useful
when quickcheck is re-exported through another crate, or vendored under a
different name. The resolved path is used everywhere the macro names
quickcheck symbols — Arbitrary, Gen, QuickCheck, TestResult, and
the injected quickcheck_assert! macro bodies.
use quickcheck_richderive::quickcheck;
mod reexport {
pub use quickcheck::*;
}
#[quickcheck(crate = "crate::reexport")]
fn t(x: u8) -> bool {
let _ = x;
true
}§Return types
The annotated fn may return anything quickcheck::Testable accepts:
bool—true⇒ pass()— never returning normally ⇒ pass;panic!/assert!⇒ failquickcheck::TestResult—passed/failed/discard/errorResult<T: Testable, E: Debug>—Err⇒ fail
No restriction beyond what quickcheck itself enforces. The
quickcheck_assert! family requires -> TestResult (or -> Result<..., ...>
where the Ok type is TestResult).
§Compile-time errors
These are caught at macro-expansion time with focused spans (covered by the
tests/ui suite):
- unknown outer key (typos of
casesetc., or any non-reserved ident — per-arg overrides are no longer outer-kv, they live on the parameter); - shape mismatch (
cases = "100"— string where an integer is expected, or vice versa); #[strategy(...)]on a destructured / pattern-bound parameter;async fn(no async test runner in upstreamquickcheck);unsafe fn, generic fns, methods (self-receivers), variadic fns, and destructuring patterns in the fn signature (use plainname: tyand rebind inside the body if you need a pattern).
§Compile-time errors
The derive reports these as compile_error! with a focused span (covered by the
tests/ui suite):
- a
uniontarget (derive(Arbitrary)supports only structs and enums); - an unknown key in a container / field / variant
#[quickcheck(...)]; withtogether witharbitraryon the same item (with = "mod"already providesarbitraryviamod::arbitrary);withtogether withshrinkon the same item (same reasoning);defaulttogether withwithorarbitraryon the same field;- an
enumwhose every variant is#[quickcheck(skip)].
§Features
This is a proc-macro crate; its features select what the generated shrink
returns:
std(default) —shrinkreturns::std::boxed::Box<dyn Iterator<…>>.alloc— for no-std consumers:shrinkreturns analloc::boxed::Box<dyn Iterator<…>>. Enable withdefault-features = false, features = ["alloc"]. Self-contained: the generatedconstblock aliasesallocinternally (extern crate alloc as <alias>;), so the consumer needs noextern crate alloc;of its own.
Because Cargo unifies features and a proc-macro is compiled once, the
std/alloc choice is workspace-global, not per-consumer: std wins if both end
up enabled, and with neither the derive emits a compile_error! rather than
guessing. If a no-std consumer is in a workspace where std gets forced on, give
that type an explicit #[quickcheck(box = "...")] pointing at a Box it can
resolve (a re-exported one, or alloc::boxed::Box with the consumer’s own
extern crate alloc; — the box path is emitted verbatim and the macro cannot
add a crate-root import). Generation otherwise uses only core paths
(core::iter::Iterator, core::clone::Clone, core::default::Default), so the
output is no-std-ready.
§Limitations
#[repr(packed)]structs are not supported. The field-derivedshrinkborrows fields (&self.field), which is invalid for a packed layout (rustcerror[E0793]: reference to packed field is unaligned). Use a non-packed type, or skip the field-derived path entirely — either with#[quickcheck(with = "module")]where the module exports botharbitraryandshrink, or with paired#[quickcheck(arbitrary = "fn", shrink = "fn")]overrides.- Edition 2018 or later is required by consumers. The generated code uses
absolute
::corepaths, which edition 2015 does not have in the crate root (it would needextern crate core;). Editions 2018 and 2021 are unaffected.
§License
quickcheck-richderive is under the terms of both the MIT license and the
Apache License (Version 2.0).
See LICENSE-APACHE, LICENSE-MIT for details.
Copyright (c) 2026 Al Liu.
Attribute Macros§
- quickcheck
- Proptest-style
#[quickcheck]attribute with per-arg generator overrides (via#[strategy(...)]on the fn parameters) and per-test config.
Derive Macros§
- Arbitrary
- Derive a native [
quickcheck::Arbitrary] implementation.