rust-variance
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Use when a lifetime coercion is refused ("is invariant over the parameter", "borrowed for 'static", "lifetime may not live long enough" on a return or argument), when deciding whether a type is covariant, contravariant, or invariant, or when adding a lifetime parameter, a PhantomData variance marker, or interior mutability to a public type. Also for subtyping of a fn item that fails a trait bound (resize_with), dyn Fn lifetime parameters, and an unbounded lifetime from a raw pointer. Triggers on "variance", "phantomdata variance", "&mut is invariant", "sender is invariant".
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- memory-modelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-borrow-semanticsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-debuggingSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-observabilitySkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-send-syncSkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-test-toolsSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 2 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
Every quoted diagnostic comes from rustc 1.98.1, edition 2024, aarch64-apple-darwin.
- Distinctive: line 36
Do not reason about variance.
- Very specific: line 45
Run one command. Pass a real output path.
- Very specific: line 77
A
'staticouter reference: Never write one into a probe. - Very specific: line 84
Use a fresh outer lifetime instead: fn p<'x, 'a: 'x, 'b: 'a>(x: &'x mut &'b u8) -> &'x mut &'a u8.
- Very specific: line 195
Never repair this with a hand-rolled contravariant handle.
- Very specific: line 272
Read the note:. It points at the bound that made the demand, not at the caller.
- Very specific: line 279
All three compile against let mut names: Vec<&str> = vec![&service];.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rust variance
The question: does a value that holds 'b fit where the compiler asks for 'a, given
'b: 'a? The type constructor around the lifetime decides, not the lifetime. Variance belongs to
type constructors only: a trait bound matches by equality, so nothing coerces through it.
Every quoted diagnostic comes from rustc 1.98.1, edition 2024, aarch64-apple-darwin.
Route the symptom to a section
| Symptom or task | Section |
| You must know the variance of a type you own | Settle it with a probe |
note: the struct X<T> is invariant over the parameter T | The variance table |
note: mutable references are invariant over their type parameter | Settle it with a probe |
An invariant type accepts a shorter lifetime through dyn or an array-to-slice change (a &mut unsize inside Cell needs 1.98; run cargo +<msrv> check) | Settle it with a probe |
A raw-pointer deref returns &'a T with no 'a in the inputs | Unbounded lifetimes |
note: raw pointer casts of trait objects cannot extend lifetimes | Unbounded lifetimes |
A Sender, handle, or queue refuses to coerce | Coerce the message, not the handle |
You add Cell, RefCell, or Mutex to a published struct (a breaking change; lock it with a compile_fail doctest and cargo test --doc) | Variance is public API |
note: requirement that the value outlives 'static introduced here, pointing at a trait bound | Traits match by equality |
error[E0597] at resize_with, map, or any callback argument | Traits match by equality |
error[E0106]: missing lifetime specifier on fn() -> &str | Three fixes |
lifetime may not live long enough on a Box<dyn Fn(&'a T)> field | A free lifetime in dyn Trait |
type annotation requires that ... is borrowed for 'static at a &mut argument | A &mut parameter |
| You script probes, or need every exact note, or the worked unsound channel | Read references/variance-tables.md |
Settle it with a probe
Do not reason about variance. Compile these three one-line functions and read the result.
// probe.rs — replace `C` with the type under test, `S` with a type that has a lifetime.
fn cov<'a, 'b: 'a>(x: C<&'b u8>) -> C<&'a u8> { x } // compiles => covariant in T
fn con<'a, 'b: 'a>(x: C<&'a u8>) -> C<&'b u8> { x } // compiles => contravariant in T
fn cov_lt<'a, 'b: 'a>(x: S<'b>) -> S<'a> { x } // compiles => covariant in 'aRun one command. Pass a real output path. Never pass -o /dev/null: rustc writes a temporary
file and renames it onto the output path. As root, which is common in containers and CI, the
rename replaces the /dev/null device with a regular file, and the command still exits 0
(rustc 1.98.1, Linux). As a normal user, the rename fails:
rustc --edition 2024 --crate-type lib --emit=metadata probe.rs -o probe.rmetacov | con | Verdict |
| compiles | rejected | covariant in T |
| rejected | compiles | contravariant in T |
| rejected | rejected | invariant in T |
Vec<T> passes cov. fn(T) passes con, so a handler for a short-lived argument serves as a
handler for a long-lived one. Cell<T> fails both, and rustc names the rule in a note:
use std::cell::Cell;
fn cov<'a, 'b: 'a>(x: Cell<&'b u8>) -> Cell<&'a u8> { x }error: lifetime may not live long enough
2 | fn cov<'a, 'b: 'a>(x: Cell<&'b u8>) -> Cell<&'a u8> { x }
= note: requirement occurs because of the type `Cell<&u8>`, which makes the generic argument `&u8` invariant
= note: the struct `Cell<T>` is invariant over the parameter `T`Two traps make a probe lie.
A 'static outer reference. Never write one into a probe. This compiles, and it proves
nothing, because &'static mut forces &'b u8: 'static and collapses both lifetimes:
fn degenerate<'a, 'b: 'a>(x: &'static mut &'b u8) -> &'static mut &'a u8 { x }Use a fresh outer lifetime instead: fn p<'x, 'a: 'x, 'b: 'a>(x: &'x mut &'b u8) -> &'x mut &'a u8.
That form is rejected with note: mutable references are invariant over their type parameter, which
is the true answer for &mut T.
An unsizing step. The { x } return is a coercion site. The trap applies when the outermost
probe type is a std pointer or cell that implements CoerceUnsized (such as &, &mut,
*const, *mut, NonNull, Box, Rc, Arc, Pin, Cell, RefCell, UnsafeCell) and it
holds dyn Trait + 'b. An unsize coercion then shortens the lifetime even in an invariant
position, so Cell<Box<dyn Debug + 'b>> coerces to Cell<Box<dyn Debug + 'a>>. Rust 1.98 adds
an inner &'b mut to the coercions that do this. A compiles verdict then measures the
coercion, not the variance. Wrap such a probe type in Vec<...>, which forwards no coercion. A
rejected verdict needs no wrapper. A probe of a type you define needs none either, because a
type of your own cannot implement CoerceUnsized on stable. Never let the output differ from
the input in anything but the lifetime (array to slice, T to dyn Trait). Read
references/variance-tables.md for the per-version table when
code that passes through such a coercion compiles on one toolchain and fails on another.
The variance table
| Constructor | Variance in T | Variance in 'a |
&'a T | covariant | covariant |
&'a mut T | invariant | covariant |
*const T | covariant | — |
*mut T | invariant | — |
Box<T>, Vec<T>, Rc<T>, Arc<T>, Option<T>, [T; N], (T, U) | covariant | — |
fn() -> T | covariant | — |
fn(T) | contravariant | — |
fn(T) -> T | invariant | — |
Cell<T>, UnsafeCell<T>, RefCell<T>, Mutex<T>, RwLock<T>, mpsc::Sender<T> | invariant | — |
dyn Trait + 'a | — | covariant |
dyn Fn(&'a T), dyn Fn() -> &'a T with a free 'a | — | invariant |
PhantomData<T>, PhantomData<&'a T>, PhantomData<fn() -> T> | covariant | covariant |
PhantomData<fn(T)> | contravariant | — |
PhantomData<*mut T>, PhantomData<Cell<T>>, PhantomData<&'a mut T> | invariant | covariant |
Two rules generate the whole table:
- A struct or enum computes variance per parameter from its fields. Uses of one variance keep
it. Uses of different variances make the parameter invariant. One
Cell<T>field makes the whole type invariant inT, and so does afn() -> Tfield next to afn(T)field. - Everything built on
UnsafeCell<T>is invariant inT, because a shared reference to it permits a write. That isCell,RefCell,Mutex,RwLock,AtomicPtr, and every channel handle instd.
A parameter that no field stores needs a PhantomData that states the intent:
PhantomData<&'a T> to borrow, PhantomData<T> to own, PhantomData<fn(T)> for
contravariance, PhantomData<*mut T> or PhantomData<Cell<T>> for invariance.
PhantomData<fn(T)> is sound only for a type that never stores or drops a T; see
Coerce the message. PhantomData<fn(T) -> T> also gives
invariance and keeps Send and Sync for every T; PhantomData<*mut T> removes both. Read
the PhantomData table in references/variance-tables.md when
the auto traits matter.
Unbounded lifetimes come from a raw pointer
An output lifetime that appears in no input is unbounded: every call site picks its own, and the
compiler agrees to anything. A raw-pointer deref is the usual source. This compiles, reads
freed memory at run time with an unpredictable result, and Miri (nightly 2026-05-15) reports
constructing invalid value of type &std::string::String: encountered a dangling reference (use-after-free):
// UB: `'a` is tied to no input, so `escaped` outlives `owned`.
unsafe fn deref_unbounded<'a, T>(p: *const T) -> &'a T { unsafe { &*p } }
fn main() {
let escaped: &String;
{
let owned = String::from("gone");
escaped = unsafe { deref_unbounded(&owned as *const String) };
}
println!("{escaped}");
}Borrowing the pointer variable is not enough. It proves that the pointer value is alive, not
that its pointee is alive. Tie the output to the actual owner, or return a guard that keeps the
allocation alive. An unsafe constructor must state and enforce the pointee-validity contract;
a borrow of *const T cannot replace it. Read
references/variance-tables.md when you need the exact-owner
accessor, which needs no dereference.
These std functions hand out an unbounded lifetime the same way: <*const T>::as_ref,
<*mut T>::as_mut, NonNull::as_ref and as_mut, as_ref_unchecked and as_mut_unchecked
(stable since 1.95), slice::from_raw_parts, and CStr::from_ptr. When no input carries the
lifetime, take &self on a wrapper that owns the pointer, or return a guard. The rust-unsafe
skill, when it is installed, owns the pointer-validity rules and the Miri workflow. Without it,
run cargo +nightly miri test --locked on every path that turns a raw pointer into a reference.
Since Rust 1.94 a raw-pointer cast cannot extend the lifetime bound of a trait object, and old
unsafe code that does fails with note: raw pointer casts of trait objects cannot extend
lifetimes. Keep 'a in the stored type. The longer bound can make a method with a
where Self: 'x bound callable through a vtable that has no entry for it
(rust-lang/rust#141402). Use transmute only when no method of the trait has a lifetime bound on
Self and the owner outlives every use of the pointer, and state both facts in the SAFETY
comment. Read references/variance-tables.md when you need the
rejected cast and the exact transmute form.
Coerce the message, not the handle
std::sync::mpsc::Sender<T> is invariant in T. Every clone is the same type, so the first call
site that demands 'static pins the whole channel, and every borrowed source fails with
error[E0597]: `storage` does not live long enough.
The message stays covariant even though the handle does not. Write each consumer as
Sender<Message<'_>>, never Sender<Message<'static>>, and coerce at the send site. When a
consumer moves the sender into thread::spawn, every producer needs Message<'static>, and no
variance trick helps: make the element type own its data. Read
references/variance-tables.md when you need the rejected and
repaired pair.
Never repair this with a hand-rolled contravariant handle. A Sender<T> whose only mention of
T is PhantomData<fn(T)> compiles, coerces to Sender<Message<'static>>, escapes into a
'static context with short-lived values still queued, and drops them after the borrow ends.
Miri reports a use-after-free in the message destructor. The same reference holds the worked
code and the exact Miri output.
Variance is public API
Wrapping a field in Cell, RefCell, Mutex, RwLock, or any UnsafeCell flips the struct
from covariant to invariant. Downstream code that shortened the lifetime stops compiling. Treat
it as a breaking change and record it in the breaking-change section of the changelog.
ConfigV1<'a> { name: &'a str } shortens from 'static to 'a. ConfigV2<'a> { name:
Cell<&'a str> } refuses the same coercion with note: the struct `ConfigV2<'a>` is invariant
over the parameter `'a` . Read references/variance-tables.md
when you need that pair as code.
Two more field changes break the same coercion. A *mut T field, or a Box<dyn Fn(&'a T)> field
in place of fn(&'a T), makes the struct invariant over 'a. A &'a mut T field is different: it
makes the struct invariant over T, and keeps it covariant over 'a, so it breaks only a type
coercion.
Code that shortens a lifetime through a &mut unsize coercion inside Cell, RefCell, or
UnsafeCell needs rust-version = "1.98". Run cargo +<msrv> check when the MSRV is lower.
Lock the variance of each public type with a lifetime parameter, so cargo check fails on the
field change before a downstream crate does. Keep the covariant probe in the crate as a private
function whose name starts with _, which keeps dead_code quiet:
fn _config_is_covariant<'a, 'b: 'a>(c: ConfigV1<'b>) -> ConfigV1<'a> { c }.
To lock intended invariance, put a compile_fail doctest on the type, in a library target, and
run cargo test --doc. A doctest compiles as a separate crate, so the type must be public and
the doctest must import it by its crate path. A compile_fail doctest passes on any compile
error, a wrong path included, so keep a compiling twin with the same import:
use std::cell::Cell;
/// ```
/// use my_crate::ConfigV2;
/// fn same<'a>(c: ConfigV2<'a>) -> ConfigV2<'a> { c }
/// ```
///
/// ```compile_fail
/// use my_crate::ConfigV2;
/// fn cov<'a, 'b: 'a>(c: ConfigV2<'b>) -> ConfigV2<'a> { c }
/// ```
pub struct ConfigV2<'a> { pub name: Cell<&'a str> }Traits match their parameters by equality
A trait bound is an equality constraint, not a subtyping constraint. F: FnMut() -> &'a str is
satisfied only by a callable whose Output is &'a str. &'static str is a subtype of
&'a str, and that fact is never consulted.
A named function has its own zero-sized item type, and the Fn impl on that type fixes
Output = &'static str. So this fails:
fn service_name() -> &'static str { "Service" }
fn main() {
let service = "Service".to_string();
let mut names: Vec<&str> = vec![&service];
names.resize_with(10, service_name);
}error[E0597]: `service` does not live long enough
6 | names.resize_with(10, service_name);
| ----------------------------------- argument requires that `service` is borrowed for `'static`
note: requirement that the value outlives `'static` introduced here
--> .../library/alloc/src/vec/mod.rs
| F: FnMut() -> T,Read the note:. It points at the bound that made the demand, not at the caller. The same shape
appears for any trait of your own, with the note on your own bound: S: Sink<&'static u8> is
not S: Sink<&'a u8>, and no variance rule bridges the two. Read
references/variance-tables.md when you need that shape as code.
Three fixes for a producer whose output lives too long
All three compile against let mut names: Vec<&str> = vec![&service];. Prefer fix 1.
fn any_name<'a>() -> &'a str { "Service" } // fix 1: unbounded output lifetime
fn static_name() -> &'static str { "Service" }
fn main() {
let service = "Service".to_string();
let mut names: Vec<&str> = vec![&service];
names.resize_with(4, any_name); // fix 1
names.resize_with(6, || static_name()); // fix 2: wrap at the call site
let f: fn() -> &'static str = static_name; // fix 3: coerce to a fn pointer
names.resize_with(8, f);
assert_eq!(names.len(), 8);
}- Give the producer an unbounded output lifetime.
fn any_name<'a>() -> &'a strlets each call site pick its own'a. Use this whenever you own the producer. In safe code the compiler checks the body for every'a. The same signature overunsafecode is the hazard in Unbounded lifetimes. - Wrap the call.
|| static_name()is a fresh closure, and inference gives itsOutputthe short lifetime the bound asks for. Use this when the producer is in another crate. - Coerce to a function pointer.
fnpointers are covariant in the return type, sofn() -> &'static stris a subtype offn() -> &'a str, and the coercion runs at the argument site. Thefnitem type has no such freedom.
Fix 3 needs the lifetime written out, because fn() -> &str has no input lifetime for elision
to use. let f: fn() -> &str = static_name; gives error[E0106]: missing lifetime specifier,
with ^ expected named lifetime parameter under the &.
A free lifetime in dyn Trait makes it invariant
dyn Trait + 'a is covariant in the 'a that bounds the object. A lifetime written inside
the trait's parameter list is a trait parameter instead, so it matches by equality, and the
object coerces in neither direction. A fn(&Event<'a>) field keeps a struct contravariant in
'a. A Box<dyn Fn(&Event<'a>)> field makes it invariant, both directions
fail with note: the struct `HooksBox<'a>` is invariant over the parameter `'a` , and the
change is invisible in review. Read references/variance-tables.md
when you need the two forms side by side.
The fix is to keep the object higher-ranked. Elision inside dyn Fn(&Event) produces
for<'x>, the struct loses its lifetime parameter, and the callback serves every caller:
pub struct Event<'a> { pub name: &'a str }
pub struct Hooks { pub on: Box<dyn for<'x> Fn(&Event<'x>)> }
fn main() {
let h = Hooks { on: Box::new(|e| { let _ = e.name; }) };
let name = String::from("tick");
(h.on)(&Event { name: &name });
}Rule: write a free lifetime into a trait object's parameters only when you intend invariance.
A &mut parameter pins the caller's lifetime
C may be covariant and still be frozen by the &mut around it. With
fn push_str_ref<'a>(v: &mut Vec<&'a str>, s: &'a str), a caller that declares
let mut v: Vec<&'static str> and pushes a borrowed &s gets
error[E0597]: `s` does not live long enough, and the label on the annotation reads
type annotation requires that `s` is borrowed for `'static` . Two repairs:
- Drop the
'staticfrom the caller's annotation.let mut v: Vec<&str>lets'ashorten to the body, and the same call compiles. - Take the container by value and give it back:
fn with_str_ref<'a>(mut v: Vec<&'a str>, s: &'a str) -> Vec<&'a str>. The&mutdisappears, so covariance applies and aVec<&'static str>coerces at the call.
In a public API whose callers may hold a longer-lived container, prefer taking the container by value, or an owned element type. Read references/variance-tables.md when you need the full rejected and repaired pair.
Related skills
Hand off to these skills when they are installed.
| Skill | Boundary |
rust-unsafe | Raw-pointer validity, PhantomData on FFI handles, the Miri workflow, and the proof for a manual unsafe impl Send or Sync |
rust-compiler-errors | E0597, E0521, and E0106 beyond the variance shapes here, and dyn compatibility (E0038) |
rust-callback-bounds | Choosing Fn, FnMut, FnOnce, or a fn pointer for a callback, once variance is settled |
rust-type-erasure | TypeId-keyed stores, dyn Any downcasting and upcasting, and the 'static bound of Any |
rust-send-sync | The auto-trait half of the PhantomData table |
rust-discipline | API review, including PhantomData<fn() -> S> on type-state tags and what counts as a breaking change |
memory-model | UnsafeCell and the atomics whose invariance this skill only cites |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

