# Is this trait sealed, or not sealed — that is the question

_Published: 2024-09-03_

*[`cargo-semver-checks` v0.35](https://github.com/obi1kenobi/cargo-semver-checks/releases/tag/v0.35.0) can determine whether Rust traits are "sealed", allowing it to catch many tricky new instances of SemVer breakage. Why is accurate sealed trait detection so important, and why is implementing it correctly so hard?*



Say our library contains the following trait:
```rust
pub trait Example: Super {
    fn frobnicate(x: crate::defs::Value);
}
```

Our coworker proposes to expand the trait with a new method:
```diff
 pub trait Example: Super {
     fn frobnicate(x: crate::defs::Value);

+    // Enables new functionality in our app.
+    fn tweak();
 }
```

Is this a major breaking change?

The answer: **"it depends."** It might be breaking, or it might not be — we can't tell yet. This essay digs into why that simple question is so tricky to answer. Spoiler: it depends on whether the `Example` trait is "sealed" — downstream crates cannot create their own implementations of sealed traits.

Good news though: `cargo-semver-checks` users can rest easy! In its latest release, `cargo-semver-checks` has become a world-class expert at applying these rules in fractions of a second. It's both faster and more accurate than humans could ever be!

Using `cargo-semver-checks` means we don't all have to invest our skill points into the minutia of SemVer in Rust. Let automation take care of it!

- [Adding new items to traits without breakage](#adding-new-items-to-traits-without-breakage)
- [New items without default values force implementation changes](#new-items-without-default-values-force-implementation-changes)
- [Sealed traits cannot be implemented in downstream crates](#sealed-traits-cannot-be-implemented-in-downstream-crates)
    - [Sealing traits via tricky function or const signatures](#sealing-traits-via-tricky-function-or-const-signatures)
    - [Sealing traits via making them impossible to name](#sealing-traits-via-making-them-impossible-to-name)
    - [Sealing traits via sealed supertraits](#sealing-traits-via-sealed-supertraits)
    - [Blanket implementations ruin everything](#blanket-implementations-ruin-everything)
- [Automation handles all this at superhuman speed](#automation-handles-all-this-at-superhuman-speed)

## Adding new items to traits without breakage

Removing items from a public trait is clearly breaking: any code that used those items is broken. It's a bit less obvious why *adding* items to a trait might be breaking — so let's break that down here 🥁

Adding items to a trait may break downstream crates by breaking *implementations* of those traits. And thus, we take our first steps down the rabbit hole:
- Are implementations required to change in order to continue implementing the trait?
- Can downstream implementations of the trait even exist in the first place?

Let's answer both questions in turn.

## New items without default values force implementation changes

The new `tweak()` trait function could have included a default implementation:

```diff
 pub trait Example: Super {
     fn frobnicate(x: crate::defs::Value);

+    // Enables new functionality in our app.
+    fn tweak() {
+        // some default implementation here
+        // which can optionally be overridden as needed
+    }
 }
```

If that were the case, the change would definitely have been _non-breaking_. Any implementations that lack a `tweak()` definition will automatically use the default defined by the trait.

Most trait items can have default values: trait constants support them on stable Rust, while trait associated types require nightly.[^sn-1]

Unfortunately, it's often impossible to define a reasonable default implementation. Our case doesn't have one, so we must keep looking.

## Sealed traits cannot be implemented in downstream crates

Rust traits can be created in a way that ensures no downstream crate can create new implementations of those traits. If there are no downstream implementations, nobody can be broken by needing to amend them. The term for such traits is "sealed traits."

Sealed traits are a deep enough topic that [I've written a guide](/blog/2023-04-05-definitive-guide-to-sealed-traits-in-rust/) covering their nuances. This essay will summarize the relevant rules as they become relevant while leaving the full details to the guide.

Our question then boils down to: Is the `Example` trait sealed?

Watch your step as we move deeper down the rabbit hole. It's a long way down.

### Sealing traits via tricky function or const signatures

One way to seal a trait is to make it impossible to write a required function (or `const` item) signature in a downstream crate.[^sn-2]

As a reminder, our trait was originally defined as:
```rust
pub trait Example: Super {
    fn frobnicate(x: crate::defs::Value);
}
```

The trait doesn't provide a default value for `frobnicate()`, so all implementations are required to implement it themselves. If the crate that defines the trait is called `upstream`, a downstream crate would have had to implement the trait like so:
```rust
impl upstream::Example for MyType {
    fn frobnicate(x: upstream::defs::Value) {
        // some implementation here
    }
}
```

We notice that writing the function signature requires naming the `upstream::defs::Value` type. But what if that type is defined like this:
```rust
// This module is private!
mod defs {
    pub struct Value;
}
```

In this case, the downstream crate cannot name this type since it cannot access the contents of a private module.[^sn-3] And if we cannot write the function signature in a downstream crate, we cannot implement the trait — it is sealed!

Associated constants offer a similar way to seal the trait. Consider a trait defined like this:
```rust
mod private {
    pub struct Unimportable;
}

pub trait ConstSealed {
    const SEAL: private::Unimportable;
}
```
Implementing `ConstSealed` for a type looks like this:
```rust
impl ConstSealed for MyType {
    const SEAL: private::Unimportable = todo!();
    //          ^^^^^^^^^^^^^^^^^^^^^
    //          The type here must match the trait definition.
    //          It's *required* to write it -- it can't be omitted.
    //          But other crates can't access the private module!
}
```
Thus, providing an `impl` for the trait requires naming the `const` item's type.
Can't name the type? You can't write an `impl` for the trait then — it's sealed![^sn-4]

In our example, say that `Value` appears to be public API that can be named and used in downstream crates, and there are no associated constants in the trait. We still don't know if the `Example` trait is sealed or not.

Deeper down the rabbit hole we go.

### Sealing traits via making them impossible to name

To implement a trait, one must first name the trait being implemented: `impl name::of::MyTrait for MyType`. If the trait's name is inaccessible, the trait can't be implemented — it's sealed.

The usual method here is defining the public trait inside a private module — the same trick we used in the previous section:
```rust
mod private {
    pub trait CannotBeNamed {}
}
```
Downstream crates cannot access the contents of the private module, so they cannot name the trait.

But be careful! If the trait is re-exported elsewhere, that re-exported name may be accessible and the trait will no longer be sealed:
```rust
mod private {
    pub trait ReexportOops {}
}

// The name `this_crate::ReexportOops` is public.
// The crate can access its own private module,
// and downstream users can access this public re-export.
//
// This trait is not sealed!
pub use private::ReexportOops;
```

As a reminder, the trait whose sealed status we're working to determine was defined as:
```rust
pub trait Example: Super {
    fn frobnicate(x: crate::defs::Value);
}
```
Say we've checked and it turns out its name is publicly accessible. We still don't know if it's sealed or not.

We must go deeper!

### Sealing traits via sealed supertraits

Implementing the `Example` trait for a type requires that the type already implement the `Super` trait first.
```rust
pub trait Example: Super {
    fn frobnicate(x: crate::defs::Value);
}
```

What if the `Super` trait is sealed? In that case, no downstream type could add an implementation for it — so they can't satisfy the prerequisites for implementing `Example` either. In that case, `Example` would be sealed.

This definition is recursive, though! To figure out if `Example` is sealed, we must first figure out if `Super` is sealed. If `Super` has supertraits of its own,[^sn-5] we might need to figure out if they are sealed too. I guess we should re-start back at the top of this essay, and check `Super` and its supertraits and their supertraits next? Oof, that'll be a lot of work.

*... much tedious effort ensues ...*

Say that we've finally discovered that `Super` is sealed. Great! Downstream code cannot add its own `Super` implementations, so it cannot implement `Example` either.

Success? `Example` is sealed, then? 😅

Hang on... What if the `Super` trait has a blanket implementation?

How many steps does this rabbit hole even have?!

### Blanket implementations ruin everything

Imagine `Super` is a *sealed* trait with a blanket implementation:
```rust
impl<T> Super for T {
    // Implementation details go here.
}
```

Any downstream types *automatically* implement `Super` thanks to that blanket implementation! Even though implementing `Example` requires implementing the sealed `Super` trait, that condition is trivially satisfied. Downstream crates are then free to implement `Example`, so `Example` cannot be sealed.

But not all blanket implementations are made equal!

For example, with the following blanket implementation, `Example` would be sealed:
```rust
// `Example` is still sealed with this.
impl<T> Super for Vec<T> {
    // Implementation details here.
}
```

Whereas with this blanket implementation, `Example` would *not* be sealed.
```rust
// `Example` is *not* sealed with this.
impl<T: Clone> Super for T {
    // Implementation details here.
}
```

How about with this one?
```rust
impl<T> Super for &T {
    // Implementation details here.
}
```
The answer: not sealed.

This post is already long enough, so I'll spare you from all the remaining edge cases here.[^sn-6] Making `cargo-semver-checks` get this exactly right required learning about Rust's [trait coherence rules](https://github.com/Ixrec/rust-orphan-rules).[^sn-7][^sn-8] Keeping it that way will require staying on top of [possible future changes](https://smallcultfollowing.com/babysteps/blog/2022/04/17/coherence-and-crate-level-where-clauses/) to that aspect of Rust. A lot of work went into making this happen!

Sorry, where were we before stumbling into the finer points of language design? Ah right, we were trying to check if our coworker's change to the `Example` trait was a major breaking change or not.

```diff
 pub trait Example: Super {
     fn frobnicate(x: crate::defs::Value);

+    // Enables new functionality in our app.
+    fn tweak() {
+        // some default implementation here
+        // which can optionally be overridden as needed
+    }
 }
```

Say we checked and `Super` doesn't have a blanket implementation. It's also sealed, so `Example` should be sealed too. Are we done yet?

Finally, yes! Our coworker's PR is not a major breaking change! 🎉

And it only us took, what, an hour of tedious labor and half a PhD in SemVer and language design to determine this... I don't know about you, but I can't wait to *never do this by hand again*!

## Automation handles all this at superhuman speed

As of [our v35.0 release](https://github.com/obi1kenobi/cargo-semver-checks/releases/tag/v0.35.0), `cargo-semver-checks` [handles all these cases correctly](https://github.com/obi1kenobi/trustfall-rustdoc-adapter/blob/052469ded6df21660dcd7965f07539ac71ae2cab/src/sealed_trait.rs). It does so in fractions of a second. Automation wins the game of SemVer trivia, and the score isn't close.

SemVer is a cornerstone of Rust development, but its rules are complex and time-consuming to master and apply. We humans are [bad at upholding SemVer on our own](/blog/2023-09-07-semver-violations-are-common-better-tooling-is-the-answer/). We're even bad at merely figuring out what the rules should be, let alone applying them![^sn-9]

It is not desirable for the entire Rust community to invest precious skill points into a PhD in SemVer. Humanity has better uses for those person-millennia of effort.[^sn-10]

`cargo-semver-checks` is already very good at what it does. It will only get better from here! It's trusted by Rust's biggest projects, and is [slated to become a built-in part of `cargo`](https://rust-lang.github.io/rust-project-goals/2024h2/cargo-semver-checks.html). We'd love [your support in making it even better](https://github.com/sponsors/obi1kenobi)! And if you aren't using it yet, we'd love to help you get started!

*If you liked this essay, consider [subscribing to my blog](/subscribe/) or following me on [Mastodon](https://hachyderm.io/@predrag), [Bluesky](https://bsky.app/profile/predr.ag), or [Twitter/X](https://twitter.com/PredragGruevski). You can also fund my writing and work on `cargo-semver-checks` via [GitHub Sponsors](https://github.com/sponsors/obi1kenobi), for which I'd be most grateful ❤*

*Discuss on [r/rust](https://www.reddit.com/r/rust/comments/1f848u9/is_this_trait_sealed_or_not_sealed_that_is_the/) or [lobste.rs](https://lobste.rs/s/kg2hba/is_this_trait_sealed_not_sealed_is).*

[^sn-1]: Last I checked, some soundness issues were still being worked on here, so the implementation isn't ready for stabilization yet. Stay tuned!

[^sn-2]: This section was updated on 2025-02-05 to add information about sealing traits using `const` items, approximately at the time when I discovered this new technique. `cargo-semver-checks` was not able to detect `const`-based trait sealing until v0.40.

[^sn-3]: Actually, even this isn't sufficient by itself. The `Value` type must be impossible to name _in any way_. It isn't enough to say that the path the trait chose to use is inaccessible: if the top level of our crate contains e.g. `pub use defs::Value`, then downstream implementations can use the re-exported path instead. Edge cases inside our edge cases!

[^sn-4]: As in the previous sidenote — any public re-export of the type is sufficient to name it and unseal the trait. This _also_ complicates the trait-sealing analysis in `cargo-semver-checks`!

[^sn-5]: And isn't already known to be sealed via e.g. a function signature.

[^sn-6]: For example, [the blanket impl loophole also works transitively](https://github.com/obi1kenobi/trustfall-rustdoc-adapter/pull/430#user-content-transitive-blankets).

[^sn-7]: And writing [an ungodly amount of cursed Rust to use in test cases](https://github.com/obi1kenobi/trustfall-rustdoc-adapter/blob/32418ddab167b456b329c83b3d53e09caa9050f4/test_crates/sealed_traits/src/lib.rs).

[^sn-8]: EDIT(2025-02-05): The ungodly amount of cursed test cases still proved insufficient. Apparently there's a way to cause _cyclic_ trait sealing relationships, such as ["trait A is sealed only if B is sealed, and B is sealed only if A is sealed."](https://github.com/obi1kenobi/cargo-semver-checks/issues/1076#issuecomment-2599567202) This was a shock to find out! Ironically, it also caused an infinite loop on a line of code with a comment saying "infinite loops here are impossible because of [...]" 😅 Fixing this required some of [the most technically-demanding 2000 lines of code I've ever written](https://github.com/obi1kenobi/trustfall-rustdoc-adapter/pull/742/).

[^sn-9]: As evidenced by the fact that after a year of research, I still needed to update this post for accuracy 6 months after publication!

[^sn-10]: If you *want* that PhD in SemVer, though, we welcome new contributors — please reach out!

Copyright (C) Predrag Gruevski 2024. [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
