Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

Semver and Evolution of Traits and Impls

Reference for rust-trait-design-reviewer · 115 lines

Programming / Rustv1.0.0Updated OCT 21 20267 references7 scripts

Which changes to traits, impls and public types break downstream code, following the Cargo book's SemVer compatibility chapter and RFC 1105 (API evolution). "Major" means a new major version (or a new minor version below 1.0); "possibly breaking" means it can break some callers in practice, usually through name collisions or type inference, and is conventionally allowed in a minor release.

Contents

Trait changes

ChangeImpactWhy
Add a method without a defaultmajorevery downstream implementor fails to compile
Add a method with a defaultpossibly breakingcalls may become ambiguous with a same-named method from another trait in scope
Add a method that makes the trait dyn-incompatiblemajordyn Trait users fail (E0038); add where Self: Sized to keep it out of the vtable
Change a method signature (params, return, receiver, generics)majorimplementors and callers both break
Add an associated typemajorimplementors must define it (associated type defaults are unstable)
Add an associated const without a defaultmajorimplementors must define it
Add a defaulted type parameter to the trait (trait T<X = ()>)minorexisting uses keep the default
Add a supertraitmajorimplementors may not implement the new supertrait
Remove a supertraitmajorcallers relying on supertrait methods through the bound break
Any change to a sealed trait's itemsminor (for items callers do not call)nobody outside the crate implements it

Seal traits that users only consume, so you can add required methods later. Leave traits open only when downstream implementations are the point (plugins, backends, codecs).

Impl changes

ChangeImpactNotes
Remove a trait implmajorcallers using the impl break
Add an impl of an existing trait for an existing typeminor, possibly breakingcan break inference: adding impl From<u16> for Id makes x.into() ambiguous where it was not
Add a blanket impl (impl<T: Bound> Trait for T) of an existing traitmajorcan overlap with downstream impls (E0119 in their code)
Add inherent methodspossibly breakingan inherent method shadows a trait method of the same name callers were using
Implement a foreign trait for a local typeminorcoherence keeps it yours

Generic bounds and parameters

ChangeImpact
Tighten a bound (T: Clone -> T: Clone + Send) on a fn, type or implmajor
Loosen a boundminor
Add a type parameter with a default to a typeminor
Add a type parameter to a fnpossibly breaking (callers using turbofish break)
Replace a concrete parameter type with a generic that still accepts it (&str -> impl AsRef<str>)minor, possibly breaking (inference)
Change impl Trait in return position to another concrete typeminor (opaque), unless auto traits change (below)

Public types

ChangeImpact
Add a variant to an exhaustive pub enummajor (exhaustive matches break)
Add a variant to a #[non_exhaustive] enumminor
Add #[non_exhaustive] to an existing enum/structmajor
Add a public field to a struct with only public fieldsmajor (struct literals and patterns break)
Add a private field when one already existsminor
Add a field to a #[non_exhaustive] structminor
Remove a derive (Debug, Clone, Hash...)major
Add a deriveminor
Change #[repr] or layout of a repr(C) typemajor

#[non_exhaustive] on public error enums and config structs (built through constructors or builders) keeps future additions minor. Callers must then write _ => arms and cannot use struct literals.

Auto traits leak

Send, Sync, Unpin, UnwindSafe and RefUnwindSafe are inferred from fields and from the concrete type behind impl Trait. They are part of your public API even though you never wrote them:

  • Adding an Rc, RefCell, raw pointer or !Send guard field to a public struct removes Send/Sync: major.
  • Changing what an -> impl Future returns so it holds a !Send value across an .await removes Send from the opaque type: major for callers who spawned it.
  • Guard against accidents with compile-time assertions (see testing-trait-designs.md):
rust
pub struct Client { inner: std::sync::Arc<std::sync::Mutex<Vec<u8>>> }

const _: () = {
    const fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<Client>();
};

Designing for evolution

  1. Seal consumer-only traits (pub trait Codec: private::Sealed).
  2. Give new trait methods defaults, and prefer names unlikely to collide.
  3. Put constructors and generic helpers behind where Self: Sized so the trait stays dyn-compatible as it grows.
  4. Mark error enums and option structs #[non_exhaustive] from the first release.
  5. Return opaque types (impl Iterator, newtypes) instead of concrete std/foreign types, so the implementation can change.
  6. Keep foreign types out of public signatures when they have their own semver (a reqwest::Error in your API ties your major version to theirs); wrap them.
  7. Implement From (not Into) and keep the set of From impls deliberate: each one is a commitment and a possible inference hazard.

Checking before release

  • cargo semver-checks compares the public API against the last published version and reports many of the breaking changes above (removed items, new required trait items, enum variants, auto-trait changes).
  • cargo public-api diff (nightly rustdoc JSON) shows the full public surface change.
  • Run downstream crates' test suites (crater-style) for widely used libraries.
  • Write the change into CHANGELOG.md with its classification.
More in Rust

rust-borrow-checker-error-explainer

Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.

rust-cargo-workspace-designer

Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.

rust-error-handling-thiserror-anyhow-designer

Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.

rust-lifetime-annotation-advisor

Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.

rust-tokio-async-service-builder

Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.

rust-unsafe-block-auditor

Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.