Is your feature request related to a problem? Please describe.
check requires both Arbitrary and Shrink, but only one of them can be derived:
pub struct Point { x : Int; y : Int } derive(Debug, @qc.Arbitrary) // ok
pub struct Point { x : Int; y : Int } derive(Debug, @shrink.Shrink) // error
// Don't know how to derive trait @shrink.Shrink for type Point
So the path of least resistance for a custom type is a hand-written no-op instance, which silently disables shrinking — the counterexample keeps irrelevant noise. Same seed, same property shape:
derived Arbitrary + no-op Shrink -> Falsified(counterexample={ x: 3, y: -8 }, shrinks=0, shrink_attempts=0)
Int (real Shrink instance) -> Falsified(counterexample=3, shrinks=0, shrink_attempts=2)
The y: -8 is unrelated to the failing predicate and would be minimized away by any structural shrinker. Nothing warns that shrinking is inert.
Describe the solution you'd like
derive(Shrink) for structs and enums:
- struct: shrink one field at a time, holding the others fixed;
- enum: shrink the payload within a constructor, optionally toward earlier constructors.
Important: for recursive types, field-wise shrinking alone is close to useless — it only ever shrinks leaves, so the structure never gets smaller and counterexamples stay large. A derived instance should also replace a node with its same-typed subterms (what Haskell's genericShrink does via subterms); the compiler knows which fields are Self-typed, and subterms are structurally smaller so well-foundedness is preserved. I would argue the feature is only worth shipping with that included.
Describe alternatives you've considered
Hand-writing instances (what we did — 97 lines for a two-type model, most of it mechanical field/entry plumbing), or accepting the no-op and losing shrinking.
Additional context
I understand this is a compiler-side change rather than a core-library one: derive is a closed set in moonc (a user-defined trait cannot be derived at all), and the table is Arbitrary, Compare, Debug, Default, FromJson, Hash, Show, ToJson. Filing here because that is where quickcheck lives and where the pain is felt — please move it if the compiler repo is the right home. Arbitrary already being in that table suggests the incremental cost is a table entry plus a codegen rule shaped like Arbitrary's.
Note that #4076 (Shrink for Map) blocks derive from being useful on map-shaped models regardless.
Verified against the 2026-08-11 toolchain. Related: #4076, #4077, #4078.
Is your feature request related to a problem? Please describe.
checkrequires bothArbitraryandShrink, but only one of them can be derived:So the path of least resistance for a custom type is a hand-written no-op instance, which silently disables shrinking — the counterexample keeps irrelevant noise. Same seed, same property shape:
The
y: -8is unrelated to the failing predicate and would be minimized away by any structural shrinker. Nothing warns that shrinking is inert.Describe the solution you'd like
derive(Shrink)for structs and enums:Important: for recursive types, field-wise shrinking alone is close to useless — it only ever shrinks leaves, so the structure never gets smaller and counterexamples stay large. A derived instance should also replace a node with its same-typed subterms (what Haskell's
genericShrinkdoes viasubterms); the compiler knows which fields are Self-typed, and subterms are structurally smaller so well-foundedness is preserved. I would argue the feature is only worth shipping with that included.Describe alternatives you've considered
Hand-writing instances (what we did — 97 lines for a two-type model, most of it mechanical field/entry plumbing), or accepting the no-op and losing shrinking.
Additional context
I understand this is a compiler-side change rather than a core-library one:
deriveis a closed set inmoonc(a user-defined trait cannot be derived at all), and the table isArbitrary, Compare, Debug, Default, FromJson, Hash, Show, ToJson. Filing here because that is where quickcheck lives and where the pain is felt — please move it if the compiler repo is the right home.Arbitraryalready being in that table suggests the incremental cost is a table entry plus a codegen rule shaped like Arbitrary's.Note that #4076 (
Shrink for Map) blocks derive from being useful on map-shaped models regardless.Verified against the 2026-08-11 toolchain. Related: #4076, #4077, #4078.