Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hierarchy of Sized traits #3729

Open
wants to merge 40 commits into
base: master
Choose a base branch
from

Conversation

davidtwco
Copy link
Member

@davidtwco davidtwco commented Nov 15, 2024

All of Rust's types are either sized, which implement the Sized trait and have a statically known size during compilation, or unsized, which do not implement the Sized trait and are assumed to have a size which can be computed at runtime. However, this dichotomy misses two categories of type - types whose size is unknown during compilation but is a runtime constant, and types whose size can never be known. Supporting the former is a prerequisite to stable scalable vector types and supporting the latter is a prerequisite to unblocking extern types. This RFC proposes a hierarchy of Sized traits in order to be able to support these use cases.

This RFC relies on experimental, yet-to-be-RFC'd const traits, so this is blocked on that. I haven't squashed any of the previous revisions but can do so if/when this is approved. Already discussed in the 2024-11-13 t-lang design meeting with feedback incorporated.

Rendered

@davidtwco davidtwco added the T-lang Relevant to the language team, which will review and decide on the RFC. label Nov 15, 2024
Co-authored-by: León Orell Valerian Liehr <[email protected]>
text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
no known size.

All type parameters have an implicit bound of `const Sized` which will be
automatically removed if a `Sized`, `const ValueSized`, `ValueSized` or
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presumably plus ~const Sized, ~const ValueSized?

What happens on ?ValueSized and ?Pointee? Remember that ?Trait is currently permitted for all traits Trait (and leads to a non-lint warning if TraitSized)

if this paragraph were to get updated, the corresponding one in section Sized bounds should be updated accordingly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! I've noted that ?ValueSized and ?Pointee would be ignored and warn like other traits.

a `const ValueSized` bound.

**Edition change:** In the current edition,`?Sized` will be syntatic sugar for
a `const ValueSized` bound. In the next edition, use of `?Sized` syntax will be prohibited
Copy link
Member

@fmease fmease Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?Sized syntax will be prohibited

What about ?Trait (TraitSized). Feels a bit weird to reject ?Sized in Rust 20XX and continue to permit all other (no-op) ?Trait bounds (that lead to a non-lint warning that can only be suppressed via allow/expect warnings). Edit: Of course, the ≠Sized-case could be moved into a separate RFC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've clarified that the entire ?Trait syntax would be prohibited after the edition migration described in the RFC (any non-Sized uses of it would just be removed as they're ignored today as far as I can tell).

text/3729-sized-hierarchy.md Outdated Show resolved Hide resolved
```rust=
const trait Sized: ~const ValueSized {}
const trait ValueSized: std::ptr::Pointee {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is a breaking change: It makes it ambiguous whether T::Metadata refers to the assoc type of Pointee or an user-provided trait:

trait Foo {
    type Metadata;
}

fn foo<T>() -> T::Metadata // does this refer to Foo::Metadata or Pointee::Metadata?
where
    T: Foo /* + Sized + ValueSized + Pointee*/
{
    todo!()
}

This can break real code and should be addressed by this RFC.

Copy link
Contributor

@zachs18 zachs18 Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC this is also why Sized (as it exists today) cannot have a const SIZE: usize associated const. Perhaps there is need for a #[bikeshed_hide_associated_item] attr applied to trait assocaited items so they can only be accessed using fully qualified syntax <T as Pointee>::Metadata; or perhaps the attr could be rustc-internal and indicate an edition before which the item is hidden as such.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch, I hadn't considered this. A simple alternative that earlier versions of the proposal had was just to introduce a new Pointee marker trait rather than re-use std::ptr::Pointee.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was anyway a proposal somewhere to expose the metadata type as Metadata<Pointee> rather than <T as Pointee>::Metadata, this would be another argument in favor of that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the RFC to make Pointee a new marker trait.

@tmandry tmandry added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Nov 15, 2024
@RalfJung
Copy link
Member

FWIW I agree with @Aloso . I tried to figure out in #2255 what the reason is for preferring "magic traits where adding one bound removes another bound" over ?Trait, but so far I didn't get it.

@davidtwco
Copy link
Member Author

davidtwco commented Nov 18, 2024

In contrast to [...], none of the traits proposed in this RFC are default bounds and therefore do not need to support being relaxed bounds, which avoids additional language complexity and backwards compatibility hazards related to relaxed bounds and associated types

This feels dishonest to me. The proposed ValueSized trait is effectively a default bound (by virtue of being a supertrait of Sized), and it can be relaxed implicitly by adding a Pointee bound.

There's nothing dishonest about this. ValueSized may effectively be a default bound, but it isn't one, and as such does not need its own relaxed bound syntax and avoids the backwards compatibility hazards that entails. This is a point of difference between this RFC and much of the referenced prior art, hence this being explicitly stated.

I would argue that this proposal, where adding a Pointee or ValueSized bound implicitly relaxes the default const Sized bound, will confuse people even more. ?Sized is explicit and it can be intuitively understood as "maybe sized". A trait bound that automatically relaxes another trait bound is neither intuitive, nor explicit.

We can agree to disagree. ?Sized is notoriously confusing for new users, and this been at least part of the motivation for the language team's historical reluctance to add new ?Trait syntax.

?Sized is definitely familiar to experienced Rust users, that's a downside of what this RFC proposes, certainly, but I don't think that it is otherwise any more or less intuitive than ?Sized syntax: it's a special-case that a user needs to learn when they want to relax the only default bound that the language has, it's just a different way to do that. It is somewhat less explicit, but it's not entirely implicit, a default Sized bound isn't just disappearing without anything written in the source to indicate that is happening, the less-strict bound will be present.

introducing new relaxed bound means downstream packages will need to reevaluate every api to see if adding : ?Trait makes sense

The same is true under this proposal. Except users need to reevaluate if adding : Sized, : const ValueSized, : ValueSized, or : Pointee makes sense.

However, the RFC states, without proof, that this is not the case:

All bounds in the standard library should be re-evaluated during the implementation of this RFC, but bounds in third-party crates need not be.

In the first comment you quote from, the discussion is around ?Trait syntax in general, in which case I would agree with it. For something like ?Leak or ?Move or any number of other proposals for new auto traits, you may need to re-evaluate APIs more readily.

However, the sentence you're quoting from this RFC is made within a larger context where it does makes sense: the specific claim that this RFC makes is that bounds do not need to be re-evaluated during implementation of the RFC.

If a bound was not re-evaluated and this feature was stabilised, and re-evaluation would have found that the bound should have been relaxed, it still could be - that's why a bound in third-party crate would not need to be re-evaluated. Furthermore, the RFC also argues that due to the nature of the specific use-cases that this RFC traits aims to support, if the vast majority of the ecosystem never re-evaluate their bounds, that wouldn't be a major issue, because use of types with these exotic sizes are likely to be localised.

the backward-compatibility may be a lie 🍰 — Relaxing the bounds in associated type, in particular FnOnce::Output, means the user of the trait will get less promises, which is a breaking change [...]

Essentially, the bounds on an associated type cannot be added (which breaks implementers) or removed (which breaks users).

This is true for ?Sized bounds, and will also be true for ValueSized and Pointee bounds under this RFC.

That's true, is noted in the RFC, and is why the RFC doesn't propose changing the bounds of any associated types to use these new traits.


It's also worth noting that the alternative approach to ?Sized isn't load-bearing to this proposal, it's still possible to introduce a hierarchy of Sized traits and keep ?Sized. I've just added a section to the alternatives elaborating on this possibility so that the language team can consider that when they discuss this RFC. I don't think that's the right approach, primarily as it doesn't scale well to the hierarchies of traits and constness that this RFC proposes, which is why it's an alternative and not the primary proposal of the RFC.

@RalfJung
Copy link
Member

RalfJung commented Nov 18, 2024

There's nothing dishonest about this. ValueSized may effectively be a default bound, but it isn't one, and as such does not need its own relaxed bound syntax and avoids the backwards compatibility hazards that entails.

The backwards compatibility issue is an actual semantic problem. Choosing different syntax cannot possibly help here. So I still don't understand why you claim that avoiding ? somehow avoids backwards compatibility issues.

There is indeed a backwards compatibility issue with ?Move, but it is not caused by ?. It is caused by having the concept of non-movable types in any way, shape, or form. Adding ?Move itself is not backwards-incompatible. Only adding ?Move to any already existing associated type is backwards-incompatible. Similarly, under your proposal, changing the bound of any associated type in the standard library to Pointee would be a breaking change. The only difference to Move is that we'd almost certainly want to add ?Move to a ton of existing associated types, but we hopefully won't want to weaken any of the existing associated types to Pointee. The syntax we use for writing these bounds doesn't matter.

If we used ? syntax to mark the opt-out, things would work exactly the same: any existing associated type keeps its existing const Sized bound, except for the ?Sized ones which get a ?Sized + const ValueSized bound (where ?Sized entirely removes all implicit sized-related bounds, and then const ValueSized adds back the bound we are looking for; other options are possible of course). This is exactly as backwards-compatible as your proposal.

It would be good to do a survey of ?Sized associated types in the standard library and figure out if any of them should be weaker than const ValueSized... but due to the inherent backwards compatibility issues, it's unlikely we'll be able to do that for any of them. The only one that comes to my mind is Deref::Target.

@RalfJung
Copy link
Member

RalfJung commented Nov 18, 2024

To be clear, my main issue here is that the RFC misrepresents the trade-off between ? syntax and the proposed syntax. As far as I can tell, this trade-off is entirely syntactical, the two options are fully equivalent in terms of backwards compatibility or any other semantic concern. If the lang team wants to pick the "magic trait bound that removes an implicit bound" over "magic ? bound", then sure whatever (I have my preference but generally try to stay out of purely syntactic discussions). But we shouldn't be under the impression that this would make any difference for the transition plan to the new hierarchy.

Specifically, there's this part here:

which avoids additional language complexity and backwards compatibility hazards related to relaxed bounds and associated types

which doesn't explain how "magic trait bound that removes an implicit bound" has less language complexity than "magic ? bound that removes an implicit bound" -- I think both have the exact same underlying complexity in terms of abstractly describing their semantics. If anything, the ?Sized version has less complexity since one can easily tell whether a bound removes implicit bounds or not. And it claims relaxed bounds have backwards compatibility hazards which are avoided by this RFC's hierarchy, which is just not correct.

And this:

Introduce a ?ValueSized relaxed bound (a user could write Sized, ValueSized or ?ValueSized) which has been found unacceptable in previous RFCs (#2255 summarizes these discussions).

I tried to find a summary in #2255 that correctly reflects the situation as it applies to this RFC, and couldn't find it.

And then this comes up in some of the items in the (extremely impressive!) detailed comparison list. For instance, these are not valid arguments I think. for the reasons mentioned above:

Downstream crates need to re-evaluate every API to determine if adding ?Trait makes sense, for each ?Trait added.

?Trait isn't actually backwards compatible like everyone thought due to interactions with associated types.

This all needs a pass to avoid misrepresenting relaxed bounds. (I'm happy to help with that, once we agree that this should be done.)

@davidtwco
Copy link
Member Author

To be clear, my main issue here is that the RFC misrepresents the trade-off between ? syntax and the proposed syntax. As far as I can tell, this trade-off is entirely syntactical, the two options are fully equivalent in terms of backwards compatibility or any other semantic concern.

I'm not arguing that the syntax that this proposes makes a difference w/r/t backwards compatibility, it doesn't. In the new section that I added earlier today in response to your concerns, I describe how this proposal could still work with ?Sized.

Specifically, there's this part here:

which avoids additional language complexity and backwards compatibility hazards related to relaxed bounds and associated types

which doesn't explain how "magic trait bound that removes an implicit bound" has less language complexity than "magic ? bound that removes an implicit bound" -- I think both have the exact same underlying complexity in terms of abstractly describing their semantics.

In the paragraph that you've quoted, all I'm arguing is that this RFC, unlike much of the prior art, doesn't introduce a new relaxed bound, like ?ValueSized, and as such avoids backwards compatibility hazards related to relaxed bounds. It does not argue that moving away from ?Sized is in any way necessary for avoiding backwards incompatibility.

There's nothing dishonest about this. ValueSized may effectively be a default bound, but it isn't one, and as such does not need its own relaxed bound syntax and avoids the backwards compatibility hazards that entails.

The backwards compatibility issue is an actual semantic problem. Choosing different syntax cannot possibly help here. So I still don't understand why you claim that avoiding ? somehow avoids backwards compatibility issues.

Likewise here, I agree, the backwards compatibility is a semantic problem, the syntax doesn't make a difference. I'm not claiming that avoiding ? will avoid backwards compatibility issues (other than that adding entirely new relaxed bounds is undesirable). I was responding to the claim that ValueSized is effectively a default bound, by making it clear that it is not a default bound, and therefore does not require a new relaxed bound.

If we used ? syntax to mark the opt-out, things would work exactly the same: any existing associated type keeps its existing const Sized bound, except for the ?Sized ones which get a ?Sized + const ValueSized bound (where ?Sized entirely removes all implicit sized-related bounds, and then const ValueSized adds back the bound we are looking for; other options are possible of course). This is exactly as backwards-compatible as your proposal.

I agree! I've written a section of the RFC that describes this possibility, I don't prefer it, but I do agree.


I think the misunderstanding here may be that the situation around relaxed bounds and backwards incompatibility may be more nuanced than I initially remembered (it's been a month or two since I wrote the prior art section and decided against introducing new relaxed bounds) - I've said in the RFC that introducing them has backwards compatibility hazards (comments like this one being fresh in mind writing that), and they do, but only in some circumstances.

That said, and correct me if I'm wrong, but neither of us are arguing for introducing new relaxed bounds, like ?ValueSized, so it's a bit of a moot point as we both agree that continuing to use ?Sized is backwards compatible and an alternative to what I propose.

I'm only arguing that ?Sized is undesirable as:

  • They're more confusing than my proposed alternative
  • They don't scale very well to constness
  • They don't scale very well to hierarchies

These are subjective, and I expect that you disagree. I added a section earlier today on keeping ?Sized as an alternative, and I'd be interested in knowing if there's anything in that you disagree with.

@traviscross
Copy link
Contributor

traviscross commented Nov 18, 2024

?Sized is notoriously confusing for new users, and this has motivated the language team's historical reluctance to adding new ?Trait syntax.

As a minor clarification, we support ?Trait syntax rather pervasively, e.g.:

trait Tr {}
fn f<T: ?Tr>() {} //~ OK

What we've been reluctant to do is to add new traits liked Sized that are implicitly added to bounds.

Has our reluctance been primarily motivated by confusion for new users? I don't know. There are other compelling reasons that would have made it difficult to add new implicitly-added bounds in the kind of cases we've previously considered, such as the well known backward compatibility problems with respect to associated types on existing traits.

@scottmcm
Copy link
Member

One reason, IIRC, is that it's backwards from how you normally think about traits. We'd generally rather that you write the easy thing, it's minimally-constrained, and if you use something in the body that needs another trait, we'll give you an error message saying that you should add the bound.

Anywhere you'd have to think "did I opt out of those 4 other things that I need to remember to think about?" is a much worse experience. That's why auto traits in libraries might never be stable, for example.

@Aloso
Copy link

Aloso commented Nov 19, 2024

@davidtwco

We can agree to disagree. ?Sized is notoriously confusing for new users, and this been at least part of the motivation for the language team's historical reluctance to add new ?Trait syntax.

If a new user sees T: ?Sized for the first time, they may be confused for a moment, then google it and find the documentation, which explains it.

If a new user sees T: ValueSized for the first time, they will not be confused because it looks familiar. They will not google it, and stay oblivious to the fact that this bound removes the default const Sized bound.

If a new user runs into an error due to a missing ?Sized bound, they see something like

help: consider relaxing the implicit `Sized` restriction
  |
2 |     type Item: ?Sized;
  |              ++++++++

I understand that this is confusing at first, but is this better?

help: consider adding a `ValueSized` bound, which relaxes the implicit `Sized` restriction
  |
2 |     type Item: ValueSized;
  |              ++++++++++++

It requires you to learn about two traits instead of one, and you still find out that Sized is a default bound and needs to be relaxed. The ?Trait syntax is not a problem, people don't struggle to learn Rust because of its syntax. Learning syntax is easy.

I'm only arguing that ?Sized is undesirable as:

  • They're more confusing than my proposed alternative
  • They don't scale very well to constness
  • They don't scale very well to hierarchies

I agree with the second point. I don't agree with the 3rd point: When I see ?Trait and Trait has a sub-trait, it is natural to assume that the sub-trait is relaxed as well. So ?const Sized means Sized, ?Sized means ValueSized, and ?ValueSized means no bounds (since there is no need for the Pointee trait). But a const ValueSized bound would have to be written as ?Sized + const ValueSized.

P.S. I just realized that ?Sized should be equivalent to const ValueSized according to this RFC, which is not as intuitive. Unless ?Trait only relaxes the trait, ?const Trait relaxes only the constness, and ?const ?Trait relaxes both. But this is pretty ugly.

@davidtwco
Copy link
Member Author

If a new user sees T: ?Sized for the first time, they may be confused for a moment, then google it and find the documentation, which explains it.

If a new user sees T: ValueSized for the first time, they will not be confused because it looks familiar. They will not google it, and stay oblivious to the fact that this bound removes the default const Sized bound.

This is conjecture, we have no reason to believe that users will only research unfamiliar syntax like ?Sized, but not unfamiliar traits like ValueSized.

Even if we suppose that your assertion holds and a user sees a parameter with a ValueSized bound and doesn't know what it is and just continues on anyway, they're likely to be able to pass whatever types they'd like to that parameter and not need to think about it. It would only be if they were writing a function, had a ValueSized-bounded parameter and tried to pass it to something like size_of that they'd run into a compilation error. That sounds like an appropriate time for a user to be introduced to that trait and need to understand it.

If a new user runs into an error due to a missing ?Sized bound, they see something like

help: consider relaxing the implicit `Sized` restriction
  |
2 |     type Item: ?Sized;
  |              ++++++++

I understand that this is confusing at first, but is this better?

help: consider adding a `ValueSized` bound, which relaxes the implicit `Sized` restriction
  |
2 |     type Item: ValueSized;
  |              ++++++++++++

These aren't significantly different. I don't believe users would find the former of these approachable and intuitive any more so than the latter.

It requires you to learn about two traits instead of one, and you still find out that Sized is a default bound and needs to be relaxed. The ?Trait syntax is not a problem, people don't struggle to learn Rust because of its syntax. Learning syntax is easy.

I agree that in learning how to relax a default Sized bound users would be introduced to new traits like ValueSized. If we went ahead with this RFC using the alternative that kept the ?Sized syntax, a user is unlikely to want a type unconstrained by all of our sizedness traits due to the limitations these have, so they'll need to add additional bounds using these new traits after using ?Sized.

I don't think it will be especially common, but a user that needs to relax Sized will be introduced to these traits regardless of whether we use ?Sized or what this RFC proposes. If users are going to be introduced to these traits anyway, then if they use ?Sized to opt-out of the default bound or what this RFC proposes is just a matter of syntax, and as you've said, syntax is easy.

Don't get me wrong, adding these traits is adding complexity to the language, but I'd argue that it is essential complexity that reflects the complexity of platforms that Rust targets, rather than incidental complexity.

@ChayimFriedman2
Copy link

There is a point that I don't see discussed here: you discuss what will be the learning effect for new users, but we also need to consider experienced user. Thus will understand both more easily, but it'll be much easier for them to learn and remember the existing ?Trait syntax, since they already know and use it.

And a related point: introducing a different way to name what is essentially the same thing introduces inconsistency to the language.

@davidtwco
Copy link
Member Author

There is a point that I don't see discussed here: you discuss what will be the learning effect for new users, but we also need to consider experienced user. Thus will understand both more easily, but it'll be much easier for them to learn and remember the existing ?Trait syntax, since they already know and use it.

Yeah, that's definitely a downside of this proposal. I think it's worth it on balance, but it's definitely a downside.

And a related point: introducing a different way to name what is essentially the same thing introduces inconsistency to the language.

I think this should be okay as the proposal removes the previous approach over an edition. It won't be entirely gone, it can't be, but it's as good as we can get it.

@cramertj
Copy link
Member

cramertj commented Nov 19, 2024

One other concern is the ability of reviewers to check for backwards-compatibility.

When reviewing a patch which removes a trait bound, I'd generally assume that doing so is relaxing the requirements on the type being bound-- a backwards-compatible change. However, this would be a rare example where removing the bound would be a breaking change, and adding the bound would be the backwards-compatible change. This is unintuitive to me.

Personally, I prefer the T: ?Trait syntax, which I read as "T may not be an instance of Trait." Relevant to this proposal, I'd also assume that T: ?SuperTrait means T: ?Trait, just as T: SubTrait means T: Trait.

@davidtwco
Copy link
Member Author

Personally, I prefer the T: ?Trait syntax, which I read as "T may not be an instance of Trait." Relevant to this proposal, I'd also assume that T: ?SuperTrait means T: ?Trait, just as T: SubTrait means T: Trait.

I discussed this with @traviscross too and added another alternative based on this, it actually ends up really quite clean and I think is a compelling alternative to the positive bounds proposal that the RFC has.

@kpreid
Copy link
Contributor

kpreid commented Nov 19, 2024

If a new user sees T: ValueSized for the first time, they will not be confused because it looks familiar. They will not google it, and stay oblivious to the fact that this bound removes the default const Sized bound.

… the proposal removes the previous approach over an edition.

I agree with the previous comments that it would be undesirable to hide the strangeness of the weakening bound behind a lack of syntax, compared to the status quo. However, I have a suggestion for a third option, if there is going to be an edition change regardless: add a new syntax which is neither a normal bound nor a removal like ?, but a “baseline” bound that nails down where we start.

Let's say the syntax is @Trait (symbol subject to bikeshedding, but we can think of it as “begin @ this point”; it could also perhaps be a contextual keyword). What it would mean is: if no baseline bound is present, the baseline bound is implicitly chosen by the edition — in all current editions, it would be Sized. In future editions, it might be something weaker or stronger. Thus,

  • <T> is “use implicit default bounds from the current edition”.
  • <T: Sized> is “use the union of the implicit baseline and Sized”, thus usually useless as today, but if ValueSized becomes the default over an edition, it strengthens the bound to Sized.
  • <T: ValueSized> is “use the union of the implicit baseline and ValueSized”, so it is useless unless an even weaker bound is made default in a future edition.
  • <T: @Sized> is “the bound is Sized, regardless of the current edition” — it matches the 2015-2024 behavior of <T>.
  • <T: @ValueSized> is “the bound is ValueSized, regardless of the current edition”.
  • <T: @SomeOtherTrait> is an error by default; the @ bound syntax can only be used if:
    • The trait is one of the traits which have ever been an implicit bound (i.e. Sized today, perhaps ValueSized in the future).
    • (Optional, if we want to allow user-defined traits to participate) SomeOtherTrait has an @ bound as a supertrait.
  • <T: ?Sized> has the 2015-2024 meaning forevermore; edition migration should replace it with @ValueSized.
  • <T: ?SomeOtherTrait> does nothing and issues a warning, as today, even if SomeOtherTrait = ValueSized, Pointee, etc — the idea is to migrate away from the ? syntax, not to expand it.

Every type variable always has either an @ explicit baseline bound, or an edition-dependent implicit baseline bound.

The advantages of this schema are:

  • The syntax tells you that something is going on, and the documentation of the trait can tell you what exactly that is.

  • It doesn’t involve any subtraction of bounds.

  • All code that uses an @ bound is now edition-change-proof; it has picked a named baseline and has opted out of all implicit bounds. This simplifies language evolution questions to the separate choices,

    • “is there room to split off a new weaker trait to serve this need?”
    • “what should the implicit baseline bound for the next edition be?”

    and “which of these things can you ? and what does that mean and is it clear?” doesn’t need to be asked. Because the baseline is named, there’s room to add another baseline without saying “this one is the correct one; we definitely got it right this time”.

  • If @ is allowed with user-defined traits (that opt in by having their own @ supertrait bound), then <T: @Foo> is a concise way to express “this is bounded by Foo, Foo’s supertraits, and nothing else”; thus, it simplifies use-cases where one must today write <T: ?Sized + Foo> in every generic parameter.

Caveat: I haven’t thought about how this interacts with const traits. Also, this is certainly adding complexity to the language; it just might be worth it to unblock extern types and thin DSTs while adding room for even more refinements to the language’s default assumptions about types.

[Update: This idea has been crossposted to https://internals.rust-lang.org/t/baseline-bounds-an-extensible-replacement-for-sized/21892 for visibility.]

Copy link

@Skepfyr Skepfyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm excited to see this RFC, I lost enthusiasm for #3396 because extern types alone didn't feel motivating enough for such an invasive change, so I'm glad to see more motivation. In general I think it's sensible, although I think it skates over a bunch of the issues that #3396 was also struggling with.


Prior to the introduction of `ValueSized` and `Pointee`, `Sized`'s implicit bound
(now a `const Sized` implicit bound) could be removed using the `?Sized` syntax,
which is now equivalent to a `ValueSized` bound in non-`const fn`s and
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true, specifically Mutex<T: ValueSized> cannot be ValueSized as conceptually it would require locking the mutex in order to call size_of_val on the wrapped type. That feels terrifying and it's currently observable that's not the case because it doesn't deadlock when you call size_of_val while holding the Mutex.

Copy link
Member

@programmerjake programmerjake Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this reminds me of C++ classes where you can figure out the size of the dynamic type by reading the vtable pointer, but, unlike Rust, that requires dereferencing the data part of the pointer to the type, whereas in Rust that info is passed as the pointer's metadata. So maybe we need both ValueSized and PointeeSized where for PointeeSized you can pass any old pointer to the type (since the metadata of a pointer must be valid but the data pointer doesn't need to), but for ValueSized you have to be able to dereference the data pointer, so takes something like &T but without the aliasing guarantees.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true, specifically Mutex<T: ValueSized> cannot be ValueSized as conceptually it would require locking the mutex in order to call size_of_val on the wrapped type. That feels terrifying and it's currently observable that's not the case because it doesn't deadlock when you call size_of_val while holding the Mutex.

Could you give the full code example of what you have in mind? I want to check that what I have in mind here exactly matches what you do.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tad conceptual as it requires types that don't exist in rust as it exists today, but would exist with something like custom DSTs. Imagine this:

// Magic syntax that means that CStr isn't Sized
struct CStr(..);

impl ValueSized for CStr {
    // Not being proposed here but could exist
    // with custom DSTs.
    fn size_of_val(&self) -> usize {
        let data = self as *const u8;
        // Find first null byte and return length
    }
}

As you can see this type has to inspect the data behind the &self pointer in order to determine its size. (Admittedly I'm not entirely convinced we'd ever want this in rust because it feels like a performance foot gun, but this matches the semantics of ValueSized as described in the RFC)

Now consider size_of_val(&Mutex<CStr>) the only way that function can execute is if the size_of_val implementation for Mutex acquires a lock on the inner data, this is bad and it's observably not the case because this code doesn't deadlock:

let mutex = Mutex::new(7);
let _guard = mutex.lock().unwrap();
size_of_val(&mutex);

The issue is that ValueSized as described doesn't match current rust's rules, where a type is only allowed to access the pointer metadata in order to determine its size. Changing the semantics to be that would mean that CStr couldn't implement ValueSized, meaning that size_of_val(&Mutex<CStr>) would throw a compile error as Mutex<CStr> also wouldn't implement ValueSized.

If traits with a `Sized` supertrait are later made const, then their supertrait
would be made `~const Sized`.

An implicit `const ValueSized` bound is added to the `Self` type of traits. Like
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels scary to me, while I agree it does match current behaviour, having most things have an implicit const Sized but traits having an implicit const ValueSized bound feels hard to teach (and remember). It also implies that no existing std traits could be implemented for ValueSized types and below (although maybe it's backwards compatible to relax that bound?).

a `const ValueSized` bound. As the `?Trait` syntax is currently accepted for any trait
but ignored for every trait except `Sized`, `?ValueSized` and `?Pointee` bounds would
be ignored. In the next edition, any uses of `?Sized` syntax will be rewritten to
a `const ValueSized` bound. Any other uses of the `?Trait` syntax will be removed as
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly suspect this is the wrong choice in most cases (for example almost all blanket impls) , I'm not sure how best to do this but I think it would be good to try to push people to use Pointee bounds where possible over the migration, otherwise they're going to feel like second class types.

a `const ValueSized` bound. As the `?Trait` syntax is currently accepted for any trait
but ignored for every trait except `Sized`, `?ValueSized` and `?Pointee` bounds would
be ignored. In the next edition, any uses of `?Sized` syntax will be rewritten to
a `const ValueSized` bound. Any other uses of the `?Trait` syntax will be removed as
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really nice to backwards compatibly allow Pointee bounds on associated types of existing trait, with Deref being the main example I can think of. I think it is possible to do this by adding implicit T::Assoc: const ValueSized bounds approximately everywhere in previous edition code. I don't know how feasible that actually is though.


If a user of a runtime-sized type or a `Pointee` type did encounter a bound that
needed to be relaxed, this could be changed in a patch to the relevant crate without
breaking backwards compatibility as-and-when such cases are encountered.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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 swede ecosystem.

This comment was marked as resolved.

Copy link

@tmccombs tmccombs Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a more self contained example consider a library with the following trait:

trait A {
  fn a<T: ?Sized>(&mut self, x: &T);
}

An impl of that trait in a different crate may call size_of_val, on x so the crate that defines A can't backwards compatibly relax the type of T to Pointee instead of const ValueSized.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you run into a similar problem with associated types. Consumers of that type might depend on being able to call size_of_val on values of the associated type, so relaxing the bound wouldn't be backwards compatible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A problematic case of this in std is Deref.

Ideally, I think that the Target type should be relaxed to Pointee, but that wouldn't be backwards compatible, because existing code might depend on being able to call size_of_val on &<T as Deref>::Target.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned here, I think that it is possible to relax associated types in a backwards compatible way, as long as we do it with the edition migration required for this RFC. However, I agree with your minimal example on a situation where I don't think there's any backwards compatible solution.

- [`unsized-vec`][crate_unsized_vec] implements a `Vec` that depends on knowing
whether a type has an alignment or not.

An `Aligned` trait could be added to this proposal between `ValueSized` and `Pointee`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the wrong place to put it, dyn Trait is ValueSized but is not Aligned, but I can imagine extern types that are Aligned but not ValueSized. I think that means Aligned could be a super trait of Sized but doesn't otherwise fit in this hierarchy. (In fact I think there's an entirely separate hierarchy including ValueAligned, etc.)


However, despite not implementing `Sized`, these are value types which should
implement `Copy` and can be returned from functions, can be variables on the
stack, etc. These types should implement `Copy` but given that `Sized` is a
Copy link
Contributor

@petrochenkov petrochenkov Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does stack allocation work for the "runtime constant" types, at binary level? Is it different from dynamic stack allocation?
Does it work for SVE specifically, or it's possible to do for other "runtime constant" types too?

I assume that the "runtime constant" value becomes available somewhere before the call to main?
Or earlier (e.g. during linking), and you need to specify the "runtime env" during the build?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.