---
title: "Design meeting 2026-03-25: Topic"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2026-03-25
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meetings.202026-03-25.3A.20Sized.20hierarchy.20migration/
url: https://hackmd.io/5mRjY1iXRoSfMZDpjhwmuw
---
> [!NOTE]
> This document is due to David Wood and lqd.
## Summary of backwards (in)compatibilities
[summary-of-backwards-incompatibilities]: #summary-of-backwards-incompatibilities
In the [RFC (up to the linked section)](https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#summary-of-backwards-incompatibilities), this proposal argues that..
- ..adding bounds of new automatically implemented supertraits of a default bound..
- see [*Implementing `Sized`*][implementing-sized]
- ..relaxing a sizedness bound in a free function..
- see [*Implementing `Sized`*][implementing-sized]
- ..relaxing implicit sizedness supertraits..
- see [*Implicit `SizeOfVal` supertraits*][implicit-SizeOfVal-supertraits]
..is backwards compatible and that..
- ..relaxing a sizedness bound for a generic parameter used as a return type..
- see [*Implementing `Sized`*][implementing-sized]
- ..relaxing a sizedness bound in a trait method..
- see [*Implementing `Sized`*][implementing-sized]
- ..relaxing the bound on an associated type..
- see [*Implementing `Sized`*][implementing-sized]
..is backwards incompatible.
### Overflow with `SizeOfVal`
[overflow-with-sizeofval]: #overflow-with-sizeofval
There is one known breaking change with this approach under the old trait
solver, due to `?Sized` introducing a `SizeOfVal` bound where it did not
previously. The types team reviewed and [FCP'd][impl_backcompat_fcp] the
experimental addition of the `Sized` supertraits, with this breaking change. It
is expected to be rare, with a single known occurrence, and is already accepted
by the next trait solver:
```rust
trait ParseTokens {
type Output;
}
impl<T: ParseTokens + ?Sized> ParseTokens for Box<T> {
type Output = ();
}
struct Element(<Box<Box<Element>> as ParseTokens>::Output);
impl ParseTokens for Element {
type Output = ();
}
```
The current trait solver has the following behaviour:
- `Element: SizeOfVal`
- `<Box<Box<Element>> as ParseTokens>::Output: SizeOfVal`
- Normalize associated type, requires `Box<Element>: ParseTokens`
- Requires `Element: SizeOfVal` cycle, goes through the non-coinductive
`Box<Element>: ParseTokens` obligation, resulting in an overflow
Without the changes described in this RFC, there was no `Element: SizeOfVal`
constraint, as `T: ?Sized` did not introduce any constraints.
This case was discovered in a crater run in the [red-lightning123/hwc]
repository, which does not appear to be on crates.io or be a dependency of any
other packages. It is tracked in issue [rust-lang/rust#143830][issue_143830]
until the new trait solver is used by default and fixes it. No other issues
about this overflow have been opened since the experiment landed on nightly, in
June 2025.
## Forward compatibility and migration
[compatibility-and-migration]: #forward-compatibility-and-migration
Trait hierarchies with a default trait can be extended in three different ways:
- [Before the default trait][hierarchy-begin]
- e.g. `NewSized: Sized: SizeOfVal: Pointee`
- This case doesn't correspond to a trait being proposed in this RFC, but is
worth considering for future compatibility, and is equivalent to `const
Sized` in [*the `const Sized` future possibility][const-sized])
- [After the default trait, in the middle of the hierarchy][hierarchy-middle]
- e.g. `Sized: NewSized: SizeOfVal: Pointee` or
`Sized: SizeOfVal: NewSized: Pointee`
- This case is concretely what is being proposed for `SizeOfVal` in this RFC
- [After the default trait, at the end of the hierarchy][hierarchy-end]
- i.e. `Sized: SizeOfVal: Pointee: NewSized`
- This case is concretely what is being proposed for `Pointee` in this RFC
In addition, for all of the traits proposed: subtraits will not automatically
imply the proposed trait in any bounds where the trait is used, e.g.
```rust
trait NewTrait: SizeOfVal {}
// Subtractive case (adding a trait bound will not weaken the existing bounds)
struct NewRc<T: NewTrait> {} // equiv to `T: NewTrait + Sized` as today
// Additive case (adding a trait bound can strengthen the existing bounds)
struct NewRc<T: Pointee + NewTrait> {} // equiv to `T: NewTrait + SizeOfVal` as today
```
It remains the case with this proposal that if the user wanted `T: SizeOfVal`
then it would need to be written explicitly.
This is forward compatible with trait bounds which have sizedness supertraits
implying the removal of the default `Sized` bound (such as in the [*Adding
`only` bounds*][adding-only-bounds] alternative).
### Before the default trait
[hierarchy-begin]: #before-the-default-trait
Introduction of a new trait, `NewSized` for example, in the hierarchy before the
default trait (i.e. to the left of `Sized`) could be one of two scenarios:
1. `NewSized` is only implemented for a kind of type that could not have existed
previously and the properties of this kind of sizedness were not previously
assumed of `Sized`
- e.g. hypothetically, if there were a hardware feature that worked only with
prime-numbered-sized types and it was necessary to distinguish between
types with this property and types without, then a `PrimeSized` trait could
be introduced left of `Sized`
2. `NewSized` aims to distinguish between two categories of type that were
previously considered `Sized`
- e.g. `const Sized` from [the `const Sized` future
possibility][const-sized], distinguishes between types with a size known at
compile-time and a size only known at runtime, both of which were
previously assumed to be `Sized`
Of these two possibilities, new traits in the first scenario can be introduced
without any migration necessary or risk of introducing backwards
incompatibilities. However, the second scenario is both much more realistic and
interesting and thus is assumed for the remainder of this section.
To maintain backwards compatibility, the default bound on type parameters
would need to change to `NewSized`:
```rust
// in `std`..
fn depends_on_newsizedness<T: Sized>() {
// Given that `NewSized` partitions existing `Sized` types into two categories,
// it must be possible for this function body to do something that depends on
// the property that `NewSized` has but `Sized` doesn't, but given that this
// is an argument in the abstract, it's impossible to write that body, so this
// comment will need to serve as a substitute
}
// in user code..
fn unaware_caller<T>() {
// A user having written this code, not knowing that `depends_on_newsizedness` exploits
// the property of `Sized` that `NewSized`-ness now represents, would need their default
// bound to change to `NewSized` so as not to break
depends_on_newsizedness::<T>()
}
```
In some instances, `NewSized` may be an appropriate default bound. In this
circumstance, a *simple migration* is necessary - see [*Simple
Migration*][hierarchy-begin-simple-migration].
However, in other circumstances, `NewSized` may be too strict as a default
bound, and retaining it as a default would preclude the use of
types-that-are-`Sized`-but-not-`NewSized` from being used with all existing Rust
code, significantly impacting the usability of those types and the feature which
introduced them.
When this is the case, there are three possibilities for migration:
1. On the next edition, `Sized` is the default bound and `NewSized` bounds are
explicitly written only where the user exploited the property that `NewSized`
types have that `Sized` types do not
- See [*Ideal Migration*][hierarchy-begin-ideal-migration]
2. On the next edition, `Sized` is the default bound and all existing `Sized`
bounds (implicit or explicit) are rewritten as `NewSized` for backwards
compatibility
- See [*Compromised Migration*][hierarchy-begin-compromised-migration]
3. Accept that `NewSized` will remain the default bound and proceed with the
migration described previously when `NewSized` being the default bound was
the appropriate option
- See [*Simple Migration*][hierarchy-begin-simple-migration]
```
┌────────────────────────────────────────────────┐
│ Is `NewSized` is an appropriate default bound? │
└────────────────────────────────────────────────┘
│ │
Yes No
│ ↓
│ ┌──────────────────────────┐
│ │ Is the "ideal migration" │─────────┐
│ │ possible/practical? │ Yes
│ └──────────────────────────┘ ↓
│ │ ┌───────────────────┐
│ No │ "Ideal Migration" │
│ ↓ └───────────────────┘
│ ┌────────────────────────────────┐
│ │ Is the "compromised migration" │──┐
│ │ possible/practical? │ Yes
│ └────────────────────────────────┘ ↓
│ │ ┌─────────────────────────┐
│ No │ "Compromised Migration" │
↓ ↓ └─────────────────────────┘
┌──────────────────────────────────┐
│ "Simple Migration" │
└──────────────────────────────────┘
```
#### Ideal Migration
[hierarchy-begin-ideal-migration]: #ideal-migration
An ideal migration would result in minimal code changes for users while
permitting maximal usability of the `Sized` types which do not implement
`NewSized`.
With this migration strategy, in the current edition, functions would have a
default bound of `NewSized`:
```rust
fn unaware_caller<T: Sized>() {
// ^^^^^^^^ interpreted as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `NewSized`
let _ = std::size_of::<T>(); // (`size_of` depends only on `Sized`, not `NewSized`)
}
```
In the next edition, assuming that the standard library's bounds have been
updated, functions would have a default bound of `Sized` and any functions which
depended on the previously implicit `NewSized`-ness of `Sized` will have been
rewritten with an explicit `NewSized` bound (and their callers):
```rust
fn unaware_caller<T: NewSized>() {
// ^^^^^^^^^^^ rewritten as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `Sized`
let _ = std::size_of::<T>();
}
```
This migration would require that the compiler be able to keep track of whether
predicates are used in proving obligations (i.e. whether the predicate from
`NewSized` as the default bound is used, or just `Sized` that it elaborates to).
rustc currently does not keep track of which predicates are used in proving an
obligation.
However, there is additional complexity to this migration in cross-crate
contexts:
A crate *foo* that depends on crate *bar* may want to perform the edition
migration first, before its dependency. A generic parameter `T`'s default bound
is `NewSized` on the previous edition, and `Sized` in the next edition, and
whether or not it is migrated to `Sized` (no textual change) or `NewSized` (now
explicitly written) depends on the uses of `T`.
Concretely, on the current edition, in the below example, `x` would have a
migration lint, and `y` would not:
```rust
fn x<T>() {
// ^ diagnostic: this parameter has a `NewSized` bound in the current
// edition, but in the next edition, this will change to
// `Sized`, you need to write `NewSized` explicitly to
// not break
std::depends_on_newsizedness::<T>()
}
fn y<T: AsRef<str>>(t: T) {
// ^ no diagnostic: `T`'s body doesn't require `NewSized`, just `Sized`,
// so doesn't need to change
let x = t.as_ref();
}
```
In the next edition, the above example would migrate to:
```rust
fn x<T: NewSized>() {
std::depends_on_newsizedness::<T>()
}
fn y<T: AsRef<str>>(t: T) {
let x = t.as_ref();
}
```
When the use of the generic parameter is in instantiating a item from a
dependency, then whether the migration lint should be emitted will depend on
whether the dependency has been migrated.
Consider the following example, when migrating crate `foo`, migration of generic
parameter `T` in functions `x` and `y` will depend on whether the generic
parameter of `bar::x` and `bar::y` have a `NewSized` bound or not. As `bar`
is not migrated, its default bound is `NewSized`.
```rust
// crate `foo`, unmigrated
fn x<T>() {
bar::x::<T>()
}
fn y<T>() {
bar::y::<T>()
}
// crate `bar`, unmigrated
fn x::<T>() {
size_of::<T>()
}
fn y::<T>() {
std::depends_on_newsizedness::<T>()
}
```
Given the default bound of the previous edition, a naive migration approach
would necessarily migrate `foo` to the strictest bounds. These stricter bounds
would in turn propagate through `foo`'s call graph, and users of the `foo`
crate, etc:
```rust
// crate `foo`, naive migration
fn x<T: NewSized>() {
bar::x::<T>()
}
fn y<T: NewSized>() {
bar::y::<T>()
}
```
An ideal migration would consider the post-migration bounds of the downstream
crate, even if it has not been migrated, which would result in the following
migration of `foo`:
```rust
// crate `foo`, ideal migration
fn x<T>() {
bar::x::<T>()
}
fn y<T: NewSized>() {
bar::y::<T>()
}
```
This introduces a hazard that within unmigrated crate `bar`, downstream crates
may begin depending on the bounds as determined by the compiler when looking at
the bodies, not the bounds as written. If `bar::x` were changed to match
the body of `bar::y`, then its external interface effectively changes even if the
signature does not. Whether or not the migration lint should be applied would
depend on whether the body has changed since the lint was introduced:
```
error: default `NewSized` bound will become more relaxed in the next edition
--> src/lib.rs:3:6
|
2 | fn x<T>
| - add the `NewSized` explicitly: `: NewSized`
3 | std::depends_on_newsizedness::<T>()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ you depend on the constness of the `NewSized` default bound here
note: in the current edition, the default bound is `NewSized` but will be `Sized` in the next edition
help: if you just changed your function and have started getting this lint, it's possible that downstream
crates have been relying on the previous interpretation of the `Sized` bound, so it may be a breaking
change to have changed the function body in the way that you have
```
#### Compromised Migration
[hierarchy-begin-compromised-migration]: #compromised-migration
If it is not possible to determine when `NewSized` would need to be explicitly
written, it would still be possible to add `NewSized` explicitly everywhere such
that the default bound can remain `Sized`. With this migration, newly written
functions would accept `Sized`-but-not-`NewSized` types.
With this migration strategy, in the current edition, functions would have a
default bound of `NewSized`:
```rust
fn unaware_caller<T: Sized>() {
// ^^^^^^^^ interpreted as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `NewSized`
let _ = std::size_of::<T>();
}
```
In the next edition, functions would have a default bound of `Sized` and all
existing implicit or explicit `Sized` bounds would be rewritten as `NewSized`:
```rust
fn unaware_caller<T: NewSized>() {
// ^^^^^^^^^^^ rewritten as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T: NewSized>() {
// ^^^^^^^^^^^ rewritten as `NewSized`
let _ = std::size_of::<T>();
}
```
While technically feasible, this migration is likely not practical given the
amount of code that would be changed.
#### Simple Migration
[hierarchy-begin-simple-migration]: #simple-migration
In a simple migration, explicitly-written `Sized` would be interpreted as
`NewSized` on the current editions, and rewritten as `NewSized` on the next
edition.
#### After "before the default trait"
[hierarchy-begin-middle]: #after-before-the-default-trait
After a trait has been introduced before the default trait (per [the parent
section][hierarchy-begin]), introducing more traits before the default trait
falls into one of two scenarios:
1. Before the leftmost trait (i.e. splitting `NewSized`)
- e.g. `NewNewSized: NewSized: Sized`
- In this scenario, introducing the new trait would be backwards compatible,
but strengthening any existing bounds to it would not without a migration
which would be more challenging without a default bound involved - this is
the same as with adding a subtrait to any other trait in user code
2. Between the leftmost trait and default trait (i.e. splitting `Sized` again)
- e.g. `NewSized: NewNewSized: Sized`
- In this scenario, the considerations is the same as in [*Before the default
trait*][hierarchy-begin]
### After the default trait, in the middle of the hierarchy
[hierarchy-middle]: #after-the-default-trait-in-the-middle-of-the-hierarchy
Introducing a new trait in the middle of the hierarchy is backwards compatible.
Future possibilities like [*Custom DSTs*][custom-dsts] suggest additions of new
traits within the hierarchy.
Stricter bounds can be relaxed to a new trait in the hierarchy, but more
relaxed bounds cannot be strengthened. For example, for a `Sized: NewSized:
SizeOfVal`, then:
```rust
fn needs_sized<T> {}
// ^ can be relaxed to `T: NewSized`
fn needs_sizeofval<T: SizeOfVal> {}
// ^^^^^^^^^^^^ cannot be strengthened to `NewSized`
fn needs_pointee<T: Pointee> {}
// ^^^^^^^^^^ cannot be strengthened to `NewSized`
```
Relaxing a bound to `NewSized` is not backwards compatible in a handful of
contexts..
- ..in a trait method
- ..if the bound is `Sized` and the bounded parameter is used as the return type
- ..if the bound is on an associated type
If `NewSized` is after the implicit sizedness supertrait then the implicit
sizedness supertrait and other traits after it can be relaxed to `NewSized` and
supertraits cannot be strengthened to `NewSized` (per the reasoning in
[*Implicit `SizeOfVal` supertraits*][implicit-SizeOfVal-supertraits]). If
`NewSized` is before the implicit sizedness supertrait then supertraits cannot
be strengthened or relaxed to `NewTrait`.
#### Implicit supertraits
[hierarchy-implicit-supertrait]: #implicit-supertraits
When a new trait is introduced after a trait in the hierarchy that is currently
the implicit supertrait - for example, `NewSized` in `Sized: NewSized:
SizeOfVal: Pointee`- then `NewSized` will either introduce a new distinction
between types that was previously assumed to be true in default trait bodies, or
it won't (depending on the nature of the distinction created by the specific
trait).
If it does, then `NewSized` will necessarily need to become the new implicit
supertrait to maintain backwards compatibility. Moving the default supertrait in
this way is backwards compatible as this problem is equivalent to [*introducing
new traits before the default trait*][hierarchy-begin].
Like introducing new traits before the default trait, implicit supertraits are
not ideal and a similar migration is possible. Concretely, an implicit
`SizeOfVal` supertrait is not ideal as it prevents all existing traits to be
implemented for `extern type`s. A migration away from an implicit supertrait
also has three possibilities:
1. An ideal edition migration would result in no implicit supertrait and would
explicitly write a default supertrait on only those trait definitions where a
default body requires it.
With this migration, in the current edition, traits would have an implicit
`SizeOfVal` supertrait:
```rust
trait Foo {}
// ^ - an implicit `SizeOfVal` supertrait
trait Bar {
// ^ - an implicit `SizeOfVal` supertrait
fn example() -> bool { std::mem::needs_drop::<Self>() }
}
```
In the next edition, traits would have an explicitly written `SizeOfVal`
supertrait only if it is necessary for the default bodies of the trait:
```rust
trait Foo {}
// ^ no implicit supertrait
trait Bar: SizeOfVal {
// ^^^^^^^^^ an explicit `SizeOfVal` supertrait is added
fn example() -> bool { std::mem::needs_drop::<Self>() }
}
trait Qux {}
// ^ this new trait added post-migration has no implicit
// supertrait
```
This migration strategy would require the same compiler support as the
[*Ideal Migration* for traits before the default
trait][hierarchy-begin-ideal-migration].
2. A compromised migration would result in no implicit supertrait and would
explicitly write a default supertrait everywhere:
In the current edition, traits would have an implicit `SizeOfVal` supertrait:
```rust
trait Foo {}
// ^ - an implicit `SizeOfVal` supertrait
trait Bar {
// ^ - an implicit `SizeOfVal` supertrait
fn example() -> bool { std::mem::needs_drop::<Self>() }
}
```
In the next edition, all traits would have an explicitly written `SizeOfVal`
supertrait:
```rust
trait Foo: SizeOfVal {}
// ^^^^^^^^^ an explicit `SizeOfVal` supertrait is added
trait Bar: SizeOfVal {
// ^^^^^^^^^ an explicit `SizeOfVal` supertrait is added
fn example() -> bool { std::mem::needs_drop::<Self>() }
}
trait Qux {}
// ^ this new trait added post-migration has no implicit
// supertrait
```
3. If no other migration is deemed feasible or practical then it is possible to
keep an implicit supertrait and accept the reduced usability of types which
do not implement it.
In the current and next editions, traits would have an implicit `SizeOfVal`
supertrait:
```rust
trait Foo {}
// ^ - an implicit `SizeOfVal` supertrait
trait Bar {
// ^ - an implicit `SizeOfVal` supertrait
fn example() -> bool { std::mem::needs_drop::<Self>() }
}
trait Qux {}
// ^ this new trait added post-migration has an implicit
// `SizeOfVal` supertrait
```
#### Associated types (e.g. `Deref::Target`)
[associated-types]: #associated-types-eg-dereftarget
It is not backwards compatible to relax the bound on an associated type, from
`type Foo: Sized` to `type Foo: SizeOfVal`, from `type Foo: ?Sized`/`type Foo:
SizeOfVal` to `type Foo: Pointee`, or with any additional sizedness traits
introduced in the hierarchy. This limits the utility of the new sizedness traits
as some operations, like a dereference, are implemented as traits with
associated types:
```rust
trait /* std::ops::*/ Deref {
type Target: SizeOfVal;
// ^^^^^^^^^ ideally would change to `Pointee`
fn deref(&self) -> &Self::Target;
}
```
If `Deref::Target` were relaxed to `Pointee` then this would result in backwards
incompatibility as in the example below:
```rust
fn do_stuff<T: Deref>(t: T) -> usize {
std::mem::size_of_val(t.deref())
//~^ error! the trait bound `<T as Deref>::Target: SizeOfVal` is not satisfied
}
```
This is not optimal as it significantly reduces the usability of `extern type`,
and limits the relaxations to `Pointee` that can occur in the standard library.
The most promising approach for migration of associated types is the same as
that being considered for other efforts to introduce new automatically
implemented traits, suggested by [@lcnr][author_lcnr] ([original
blog][blog_lcnr_implicit_auto_traits]). This ideal migration would defer checks until
post-monomorphization in rustc. For example, after `Deref::Target` is relaxed to
`Pointee`, `bar` would normally stop compiling, but instead this would continue
to compile and emit a future compatibility warning:
```rust
fn foo<T: Deref>(t: T) -> usize {
std::mem::size_of_val(t.deref())
//~^ warning! `T::Target: SizeOfVal` won't hold in future versions of Rust
}
fn bar<T: Deref>(t: T) -> usize {
std::mem::size_of_val(t) // no warning as `Deref::Target: SizeOfVal` is not needed
}
```
On the next edition, this can stop being a future compatibility warning and we
can have migrated users to write a bound on the associated type only when
it was required:
```rust
fn foo<T: Deref>(t: T) -> usize
where <T as Deref>::Target: SizeOfVal
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added as part of ideal migration
{
std::mem::size_of_val(t.deref()) // okay!
}
fn bar<T: Deref>(t: T) -> usize {
// no migration as `Deref::Target: SizeOfVal` was not needed
std::mem::size_of_val(t)
}
```
If this is not feasible, a compromised migration with more drawbacks, is to
elaborate the existing `SizeOfVal` bound in user code over a migration, such as:
```rust
fn foo<T: Deref>(t: T) -> usize
where <T as Deref>::Target: SizeOfVal
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added as part of compromised migration
{
std::mem::size_of_val(t.deref())
}
fn bar<T: Deref>(t: T) -> usize
where <T as Deref>::Target: SizeOfVal
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added as part of compromised migration
{
std::mem::size_of_val(t)
}
```
This approach is not optimal, however:
- It results in a lot of churn when migrating, and for cases that may not
always be of interest for a given project
- While the migrated code would keep working, the implicit defaults of the
previous edition would be explicitly brought over, even if the new edition
defaults have weaker requirements
- This doesn't make `extern type` any more usable with existing code, and in
many cases, the explicit bounds introduced would be stricter than required
Furthermore, this wouldn't work in the general case with non-sizedness traits
(as would be useful for other ongoing RFCs), as it could cause infinite
expansion due to recursive bounds:
```rust
trait Recur {
type Assoc: Recur;
}
fn foo<T: Recur>()
where
// when elaborated..
T: Move,
T::Assoc: Move,
<T::Assoc as Recur>::Assoc: Move,
<<T::Assoc as Recur>::Assoc as Recur>::Assoc: Move,
...
{}
```
This limitation does not affect sizedness traits as they do not have associated
types themselves.
It may be possible to refine this to run probes in the trait solver at migration
time, using obligations with relaxed bounds, and to compare the results. This
seems hard to make workable in the general case, and could also run into
slowness issues depending on the number of combinations of places to check and
number of options to try at each one.
If none of the above approaches are deemed feasible, the status quo with regards
to relaxation of bounds on associated types could be maintained and this
proposal would still be useful, just slightly less so.
### After the default trait, at the end of the hierarchy
[hierarchy-end]: #after-the-default-trait-at-the-end-of-the-hierarchy
All of the same logic as [*After the default trait, in the middle of the
hierarchy*][hierarchy-middle] applies. Future possibilities like
[*externref*][externref] suggest additions of new traits at the end of the
hierarchy.
[author_lcnr]: https://github.com/lcnr
[blog_lcnr_implicit_auto_traits]: https://lcnr.de/blog/2025/11/28/implicit-auto-traits-assoc-types.html
[issue_143830]: https://github.com/rust-lang/rust/issues/143830
[impl_backcompat_fcp]: https://github.com/rust-lang/rust/pull/137944#issuecomment-2912207485
[red-lightning123/hwc]: https://github.com/red-lightning123/hwc
[implementing-sized]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#implementing-sized
[implicit-SizeOfVal-supertraits]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#implicit-sizeofval-supertraits
[const-sized]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#const-sized
[adding-only-bounds]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#adding-only-bounds
[custom-dsts]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#custom-dsts
[externref]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#externref
---
# Discussion
## Attendance
- People: TC, Niko, Jack, Josh, Rémy, David Wood, Tyler Mandry, Mark, James Muriuki, Frank Steffahn, Zachary Sample, Aapo Alasuutari, Yosh, Xiang, Nurzhan Saken
## Meeting roles
- Driver: TC
- Minutes: Nurzhan
## Vibe checks
### Josh
Very thorough analysis and walkthrough.
I agree that the "ideal" migration would be, well, ideal, if we can manage it.
The one caveat is what I noted below regarding public APIs; we don't want crates to silently cede future design space by weakening bounds on their public APIs.
That aside, if we can't do the "ideal" migration, I also agree that the "compromised" migration would be excessive and painful; it'd introduce far too many unnecessary bounds where none previously existed. I hope that paying attention to the cases that *explicitly* write `Sized` or `?Sized` will end up being enough.
I think doing whatever portion of "ideal" we can manage, and otherwise doing the "simple" migration, would be fine.
At a higher level, I would also say that having seen the detail put into these documents and this analysis, I trust the folks working on this to manage the transition with care for users, and I would trust the recommendation they make. That applies even if their recommendation ends up differing from the vibes I just wrote.
### Niko
I'm trying to decide what is useful as a "vibe" and I am not sure. I guess my *vibe* is that I think we as the lang team should not micromanage this transition. I think we should weigh in but generally trust the owners (davidtwco et al.) to drive this thoughtfully and with care. I'm ~ok with any version of the transitions, but I do like the lcnr-proposal, and I wonder if we can't use it more broadly ([see below](https://hackmd.io/5mRjY1iXRoSfMZDpjhwmuw?both#Cant-we-use-the-lcnr-approach-in-more-places)).
### Tyler
I'm unclear on what we're being asked to decide. For my part I think we should discuss any implications of the migration on user code, and vibe check the post-mono error approach. I'm +1 on trying that approach and seeing how it works, though I feel like I need to give it another read-through to understand the implications.
### TC
This gets into all of the hard problems we know about for migrating trait hierarchies, e.g., the hard problem of relaxing bounds on associated types. So this makes me think of the broader solutions we've talked about for this. I too am interested in lcnr's proposal. I appreciate the work that went into this document. At the same time, I'm not yet feeling confident in which of the possibilities are being concretely proposed to us or that all of the problems that would need to be solved are solved with these.
This makes me think of the original bikeshed story. The story goes that someone is bringing a proposal for a nuclear plant to a city council. The company brings in people to talk through containment, water treatment, backup power, and other details. People's eyes glaze over, and they say, essentially, "well, you folks are the experts". And then the company mentions that they need to put a bikeshed in the back...
I don't want to be that city council. We shouldn't micromanage this, but I do want to understand the details, and I'm not yet feeling that here.
### Jack
I sort of lost the plot of the document pretty early on, and even after re-reading parts of it, I still feel lost as to the "high-level" goal/expectation here. (Of course, I can make guesses from what I've seen, but I don't see it explicitly spelled out.) In general, I agree with Niko and Josh that it's clear that a lot of thought has gone into this, and ultimately I think the owners of this work know what the options are and the tradeoffs that exist. So on that note, I think digging into those details as a lang team seems "too in the weeds". Rather, I want us to be thinking about and deciding on the high-level goals we want and expect here, and trust the owners to figure out the right path to get us there.
Niko: Jack's comment helped me think through TC's comment. I think what could be useful for us as a team, for all parties, if we have to compromise A or B... Might be nice if we could say that.
Remy: It's less about what's in the document and more about if we heard lang's concerns correctly. We've described this as a way to move trait hierarchies abstractly. ?? All the tooling that we'd have to develop might be helpful for this RFC. We just want to know if we're aligned with your expectations.
Tyler: We should talk about various tradeoffs we mmight need to do in theory. I agree that we need more investigation. I'm not comfortable drawing a hard line now, but would be good to provide some guidance.
### ...
## Public APIs
Josh: Do we need to take public APIs into account? Even in the "ideal" case, do we *want* to automatically migrate a public API to a weaker bound? That is a backwards-compatible change, but it means the crate cedes future design space if it ever wants that bound. One advantage of always migrating to the stronger bound: nobody gets surprised by ceding future access to the `NewSized` capabilities.
## Terminology nit: Meaning of forward-compatible
TC: As I raised on the RFC thread, I believe it's confusing and not correct to say that a state X is forward compatible with some state Y if it requires an edition migration to get there. It'd better to say that state Y can be reached from state X with an edition migration.
nikomatsakis: "Edition-compatible" perhaps? We should define a clear terminology here and put it up in a glossary on rust-lang.github.io/lang-team.
## Breaking down compatibility and migrations
nikomatsakis: I think it might be useful to talk out "compatibility" and the impact of it. Let me think now to quantify this.
* "In any order" -- an "in any order" timetable implies that crates can migrate to the new edition in any order.
* in contrast, "dependencies first" means that if I migrate to the new edition, but my dependencies have not, then I will be forced to use stronger bounds than I probably need (e.g., I need to use `NewSized` not because I need that extra capability but because my dependency hasn't migrated yet)
* "Precise" -- means that when you are done migrating, you have the "right-sized" bounds (you only have `NewSized` if you really NEED `NewSized`)
* "Coarse" -- means that we give you bounds that will *compile* but may not be "right-sized"
* Up-front vs Post-mono -- usual meaning
Tyler: I was confused by the use of backwards-compatible and compatible. I think this is about language changes, and backwards-incompatible is about what needs an edition?
David: I think at the start, backwards-compatible, we meant what we can change without breaking user code, and in the rest of the doc, by forward compatible we meant what doesn't close doors.
TC: When working on the Reference, we can sometimes find ourselves tripping over terms a bit. Sometimes you get yourself in a knot over it, and it's helpful to say, "forget it," and stop trying to be concise and get really explicit. That often works well. This is probably what I would do here -- simply spell it out and avoid leaning on terms that might be confusing in this context.
Niko: I'll put forward two terms (see above).
Tyler: I think the terms are helpful. It gets into the exact tradeoffs we're making here.
## Can't we use the "lcnr approach" in more places?
nikomatsakis: I'm a bit confused about the so-called "ideal" migration. The document says
> With this migration strategy, in the current edition, functions would have a
default bound of `NewSized`:
>
```rust
fn unaware_caller<T: Sized>() {
// ^^^^^^^^ interpreted as `NewSized`
std::depends_on_newsizedness::<T>()
}
fn another_unaware_caller<T>() {
// ^ interpreted as `NewSized`
let _ = std::size_of::<T>(); // (`size_of` depends only on `Sized`, not `NewSized`)
}
```
but then notes that doing the migration, even if we can do it "ideally", will result in the fact that old-edition crates have stronger bounds than they ought to. This is technically allowed but weakens the Edition's promise of "upgrade on your timeline", since if you upgrade before your dependencies have done so, you will need to add a lot of extra bounds that later become unnecessary (presuming I'm understanding).
It seems like there is an alternative, which is to leverage lcnr's idea here too -- i.e., interpreted `Sized` as `Sized` everywhere, but allow uses that require `NewSized` to continue compiling. If in fact a type is given that is not usable, you get a post-mono error, which is annoying, but is guaranteed not to exist in older code. And when people go to the new edition, we give them the *right* bound, and things eventually settle down.
Am I missing something?
tmandry: I also had a question about this. After an example showing `Move` does not work, the doc says:
> This limitation does not affect sizedness traits as they do not have associated types themselves.
But `Move` doesn't have an associated type either, so I'm confused how that is relevant to the example and why the example doesn't apply to sized traits.
Josh: That makes sense. To echo something back, code written in the old edition will not make use of post mono error because you can't have types not meeting the requirements. Once you write code in the new edition, you might get a post mono error if you have a mixed-edition program. Later, ??, you won't get the error.
Niko: I don't think that's quite right. ??, but only new code...
Josh: I see. We could introduce a new mechanism that allows creating types that don't meet the requirements, and say it doesn't exist in the new(?) edition. ??
Niko: One other thing: even though we will let it compile, we'll warn on usage of this extra bound. If you need Sized but you have const Sized, we'll warn you, which should nudge people to migrate.
David: the lcnr approach and ?? are trying to achieve the same thing. In both cases, we want migration to happen in any order. I think the assumption that code won't cause errors now because it makes ?? doesn't hold. There's code today that assumes constness of size ?? distinction doesn't exist. We could introduce a distinction ??. I wanted to get a vibe check from lang on this migration. If we wanted to have an any order migration, we'd have to assume that our interpretation of the bound was what we could take as a given. We're creating a contract with the user ??, and we're interpreting it as a weaker version of what they've written.
Tyler: Let's start with something concrete: This is meant to handle existing code like
```rust
// Compiles today:
fn foo<T: Deref>(x: &T) -> usize {
size_of_val(x)
//~^ FCW: You need `T: Deref<Target: SizeOfVal>`; this will break in future versions of Rust.
}
```
Tyler: I expect this migration to be mostly for assoc types. We want to weaken that bound (?Sized ??). I'm starting from the assumption that this will be uncommon, and the benefit of this is that we can do the migration in any order (theoretically). If your dependency migrates first, your code will still compile; if you migrate first that's fine. We'll FCW regardless of your dependency. I like the idea that we'll warn at every step of the chain.
Niko: You said there exists code that requires const Sized today, and therefore the lcnr thing doesn't apply in the same way, right?
David: The lcnr approach works for assoc types and maybe other cases. Both the lcnr and the other approach do the same thing.
Niko: The question is whether there are types that are Sized today but will not become const Sized (?).
David: There are still functions that assume that property (that Sized => const):
```rust
fn foo<T: Sized>() { // should I lint?
bar2::<T>() //
}
// another crate..
fn bar1<T: Sized>() {
const { size_of::<T>() }
}
fn bar2<T: Sized>() {
size_of::<T>()
}
```
Niko: I think this is the thing that's being compromised. That we're making function body in old editions be ??, and we're mitigating it via post-mono error (?). You shuld be able to move the bound to a stronger bound, even if backwards-incompatible, because nobody's using it (?)
Tyler: We're adding new semver surface area to crates that didn't know they had it. There's a transition period during which you can add a `const Sized` bound even if you don't use it today, but after that period, adding the bound would be a breaking change.
Niko:
* A depends on B depends on C
* C does `const { size_of::<T> }`
* Initially:
* C gets a *FCW* but *presents* a bound of `Sized` (not `const Sized`)
* C fixes that FCW, changes their bound froem `Sized` to `const Sized`
* theoretically, this is a breaking change -- stronger bound
* however, in practice, it would've been a post-mono error had anyone instantiated C with a type that does not meet this bound
* B starts to get a FCW now -- because C is demanding `const Sized` but B only requires `Sized`
* B changes to `const Sized`
* same reasoning as above -- not a breaking change
* A just keeps working, because the types it was using were both `Sized` and `const Sized`
* we know this beacuse: (1) there are no types that don't meet that description
* and (2) A compiled without a post-mono error
* Eventually:
* we align all editions
Tyler: I would warn in B from the beginning, without waiting for C to fix its bound.
Niko: Yeah, that's probably better.
Josh: 1) David asked if this is a tool we can use and got a reaction "it seems fine, our usual aversion to post-mono doesn't apply here because it's for a transition". 2) Niko's "You should be able to move the bound to a stronger bound because nobody's using it" -- the most important point is bck-compat, but the close second is the degree to which we add complexity to code that wasn't there. I'm concerned about code that had written `: Sized` or `: const Sized`, but also ??, that is going to verbosify a lot of code. That's one of the things that makes me feel like the post-mono is worth doing.
Niko: Josh, to some extent that's inevitable that there will be code that writes `T` which we'll have to write `T: const Sized`... the assumption is not a lot of code, but it can't be a hard rule that we never add bounds.
Josh: I'm suggesting ?? only the code that needs it writes it. Imagine if we elaborated every implicit Sized bound across the ecosystem and how horrible it would be. If we have to be cautious and mark more things than actually needed, ??. But ??, the less compplexity we add to programs.
---
TC: What we have here is a trait hierarchy migration. It makes me wonder the degree to which we need to do special-case handling for this versus the degree to which we could add things that would generally help trait hierarchy migrations.
TC: I've previously suggested that maybe we could break apart two things for associated types: 1) the minimum that an implementor is allowed to implement and 2) the minimum that someone can assume by default when the trait appears in the bounds. Would that at all be helpful here?
```rust
// State 1, before relaxation.
trait Tr {
type Assoc: DoubleEndedIterator,
}
// State 2, after relaxation.
trait Tr {
type Assoc: Iterator default DoubleEndedIterator,
}
// In both states:
fn f<T: Tr>() { // Desugars to include `where <T as Tr>::Assoc: DoubleEndedIterator`.
// Can assume that `<T as Tr>::Assoc: DoubleEndedIterator`.
}
```
David: The same migration case occurs with what we assume about Self in default body and default bounds when we extend the hierarchy to the left. This would have to scale with those circumstances. (?)
```rust
trait Foo {
fn foo(&self) { size_of_val(self) } // assumes `Self: SizeOfVal` -- this would require a very similar migration as the associated traits to make code that uses this assumption explicit so it isn't just the default
}
// and with
fn foo<T>() { const { size_of::<T>() } } // similarly, assumes `T: const Sized`, but we want `T: Sized` -- same migration as associated traits, more or less
// the currently proposed migration mechanism solves these cases + associated types, but the proposed associated type mechanism wouldn't??
```
Niko: This proposal is kind of like the Sized default today. There's some code that could have a ?Sized bound but doesn't because no need, and we're ok with that because we'd have to write Sized everywhere otherwise. This might occur in assoc type position in traits?
Niko: There are 2 situation: 1) the bound is stronger, but the user wants the weaker bound; 2) the bound is ?? but the user wants the stronger bound. ?? would be useful in scenarios where people want the stronger bound.
TC: In suggesting it I'm not really arguing which of these users will want more often at use sites. I'm suggesting that breaking these apart makes it possible for us to immediately allow more implementors (i.e., implementors of the relaxed bound) without breaking any current implementors or any current use sites. I.e., it allows us to do it at all.
Niko: In my terminology it enables coarse migration in a painless way, and not precise migration.
TC: We would then make the default edition-dependent.
Tyler: This is useful to introduce the feature since we could introduce it coarsely, and layer the migration strategy on top later on.
---
Frank: One thing that differentiates bounds on associated types to be more than
than "just" like desugared/implied bounds is the fact that they can apply recursively. E.g.:
```rust
// currently, `Assoc` is implicitly `SizeOfVal` here.
trait Tr {
type Assoc: Tr;
}
// So here, we have all of:
/*
T::Assoc: SizeOfVal
T::Assoc::Assoc: SizeOfVal
T::Assoc::Assoc::Assoc: SizeOfVal
T::Assoc::Assoc::Assoc::Assoc: SizeOfVal
…
… and so on
…
recursively, so it's more than just a "desugaring"
*/
fn foo<T: Tr> {
}
```
(The meeting ended here.)
---
Frank: I'm wondering if crates could migrate "conditionally", so the migration work can happen in parallel, but actually depending on it still requires all crates in the dependency chain to have opted in:
```rust
// migrated; author of this crate
// pre-anticipates that `other_crate::bar2` will likely get a relaxed bound
// [relaxing bar2<T: const Sized> to bar2<T: Sized>]
// (and compiler lints could perhaps even *generate* this conclusion by inspecting the code).
fn foo<T: Sized>()
/* pseudo syntax … for conditional bound */
where T: `const Sized if other_crate::bar2<T> requires T: const Sized`
{
bar2::<T>() //
}
// ------------
// `another_crate`.. not yet migrated, but expressed in new syntax
// (currently the default *is* `const Size`)
fn bar1<T: const Sized>() {
const { size_of::<T>() }
}
fn bar2<T: const Sized>() {
size_of::<T>()
}
```
## Mitigations?
TC: The document notes the following unresolved items:
> Relaxing a bound to `NewSized` is not backwards compatible in a handful of contexts..
>
> - ..in a trait method
> - ..if the bound is `Sized` and the bounded parameter is used as the return type
> - ..if the bound is on an associated type
This is related to an item raised in the RFC thread [here](https://github.com/rust-lang/rfcs/pull/3729#discussion_r1851857461):
> I think this is an overly rosy outlook. It may be very annoying for types that aren't `const ValueSized` to be locked out of implementing traits from other crates as it will make it much harder for them to be used like normal types, also we'd need to teach crate authors to write new code as permissively as possible. Additionally it's not always going to be possible to relax these bounds without a breaking change, my personally scariest trait is [serde's `Serializer` trait](https://docs.rs/serde/latest/serde/trait.Serializer.html#tymethod.serialize_some) as it's impossible to relax that bound as serializers might rely on it but it prevents these new types from being first class members of the serde ecosystem.
This still seems a serious problem. Maybe I missed it in the document -- what's the proposed mitigation?