--- 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?