-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: León Orell Valerian Liehr <[email protected]>
text/3729-sized-hierarchy.md
Outdated
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 |
There was a problem hiding this comment.
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 Trait
≠ Sized
)
if this paragraph were to get updated, the corresponding one in section Sized
bounds should be updated accordingly
There was a problem hiding this comment.
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.
text/3729-sized-hierarchy.md
Outdated
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 |
There was a problem hiding this comment.
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
(Trait
≠ Sized
). 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.
There was a problem hiding this comment.
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
```rust= | ||
const trait Sized: ~const ValueSized {} | ||
const trait ValueSized: std::ptr::Pointee {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There's nothing dishonest about this.
We can agree to disagree.
In the first comment you quote from, the discussion is around 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.
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 |
bdbbb19
to
5f4b8ff
Compare
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 There is indeed a backwards compatibility issue with If we used It would be good to do a survey of |
To be clear, my main issue here is that the RFC misrepresents the trade-off between Specifically, there's this part here:
which doesn't explain how "magic trait bound that removes an implicit bound" has less language complexity than "magic And this:
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:
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.) |
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
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
Likewise here, I agree, the backwards compatibility is a semantic problem, the syntax doesn't make a difference. I'm not claiming that avoiding
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 I'm only arguing that
These are subjective, and I expect that you disagree. I added a section earlier today on keeping |
As a minor clarification, we support trait Tr {}
fn f<T: ?Tr>() {} //~ OK What we've been reluctant to do is to add new traits liked 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. |
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. |
If a new user sees If a new user sees If a new user runs into an error due to a missing
I understand that this is confusing at first, but is this better?
It requires you to learn about two traits instead of one, and you still find out that
I agree with the second point. I don't agree with the 3rd point: When I see P.S. I just realized that |
This is conjecture, we have no reason to believe that users will only research unfamiliar syntax like Even if we suppose that your assertion holds and a user sees a parameter with a
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.
I agree that in learning how to relax a default I don't think it will be especially common, but a user that needs to relax 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. |
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 And a related point: introducing a different way to name what is essentially the same thing introduces inconsistency to the language. |
Yeah, that's definitely a downside of this proposal. I think it's worth it on balance, but it's definitely a downside.
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. |
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 |
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. |
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 Let's say the syntax is
Every type variable always has either an The advantages of this schema are:
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.] |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 beValueSized
as conceptually it would require locking the mutex in order to callsize_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 callsize_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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
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 theSized
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 ofSized
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