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

Make trait methods callable in const contexts #3762

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

Conversation

oli-obk
Copy link
Contributor

@oli-obk oli-obk commented Jan 13, 2025

Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.

Rendered

Tracking:

@juntyr
Copy link
Contributor

juntyr commented Jan 13, 2025

I love it and I’m very excited to get to replace the outlandish const fn + associated const hacksworkarounds I’ve joyfully come up over the last years :D

Thank you for all of the hard work that everyone working on the various implementation prototypes and around has put into const traits!

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jan 13, 2025
@letheed
Copy link

letheed commented Jan 13, 2025

I didn’t see const implementations in alternatives. Is there a reason they can’t be considered ?

Eg. const impl PartialEq for i32 { … } where only this impl is const, no changes to the trait.

const fn foo<T: Trait<bikeshed#effect = ~const> + OtherTrait<bikeshed#effect = const>>(t: T) { ... }
```

## Make all `const fn` arguments `~const Trait` by default and require an opt out `?const Trait`
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs to be expanded on quite a bit more in the main text.

Up until reading this section, I actually hadn't really considered the examples below. I think it's really important describe better the different ways you might use a const (or maybe const) trait both inside and outside of const contexts. The main text does a bit to motivate the need to distinguish between const and maybe-const bounds, but does not really go into why you need the distinguish between maybe-const and not-const bounds. The first example below hints a bit at it, but in my opinion is pretty incomplete. For my own sake, I expanded the first example below to make it work on nightly and to show how the code must change between going from with non-const bounds (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=25b7e140187aca91a5ce377b42f86140) and without (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=7ce4396310758e4595704e4228f45d69). Now, what this doesn't do, for me, is show why being able to write the non-const bound version is actually useful. A real-world example would go far here.


Now, going into this RFC, I was strongly in favor of this alternative (and still do favor it, though am slightly convinced by the associated const example below), for two reasons:

First, I expect the maybe-const bound to be what users want in the overwhelming majority of cases. In that sense, having "extra" noise to the syntax of bounds results in a reduced user experience. Though, as a counterpoint, the explicitness of them being "different" from what you see elsewhere is nice.

Second, ~const bounds are new weird syntax. And, for many, Rust already has a lot of weird syntax, so we should be wary about adding more. On the contrary, users are already used to ?Sized bounds, so ?const bounds is not that weird or different. I am very unconvinced by argument below that this ~const syntax is "the one folks are used to for a few years now": 1) I expect plenty of people that will eventually use this don't actually use this on nightly 2) being "used to" a syntax does not automatically make it best.

Copy link
Contributor

@tgross35 tgross35 Jan 14, 2025

Choose a reason for hiding this comment

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

Second, ~const bounds are new weird syntax. And, for many, Rust already has a lot of weird syntax, so we should be wary about adding more. On the contrary, users are already used to ?Sized bounds, so ?const bounds is not that weird or different. I am very unconvinced by argument below that this ~const syntax is "the one folks are used to for a few years now": 1) I expect plenty of people that will eventually use this don't actually use this on nightly 2) being "used to" a syntax does not automatically make it best.

I was going to write similar will piggyback on this instead. ~const reads to me very strongly as "not" or something along those lines, since ~ is bitwise not in C/++ and indicates a destructor in C++, and I think this is likely to be more understood by users than ~ meaning maybe. Not that we should necessarily base any syntax decisions off of C/++, but I don't know that a percentage of users being familiar with a nightly syntax makes that strong of a case either.

The ? reads much stronger to me as "maybe", as in ?Sized = "maybe sized" and ?const = "maybe const".

This syntax was discussed recently at https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/Nadri's.20effect.20elision, and reasons came up for using ~ over ?. However, reading again, I think the reasoning had the assumption that if ~const were changed to ?const then the exact semantics should be updated to nearly the same as ?Sized. That is, changing the syntax would imply const trait by default and ?const trait opts out, similar to Sized by default.

Imo the correlation doesn't need to go that deep though: we can say "read ? as "maybe' ", or "?X opts out of the default state of X", i.e. use the ? sigil with const without changing any behavior here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, in an ideal world we'd do that, I mean, my original RFC from years ago proposed that. But that needs new syntax considering how many ppl are using nonconst bounds on const fns (and not just Copy bounds).

You need those when you just need access to assoc consts or types, or when your struct has bounds, as you need to replicate those on the impls, even if the specific const fns you wrote don't need them.

The reason we gave up on ?const was that we messed up the impl, because we made the impl try to mirror that. Today's impl is ~const, opting into the constness, which is much simpler impl wise. We can invert the syntax, but the impl is opposite that and just how traits work. Yes traits have ?Sized, but that's a thing we regularly get wrong somewhere in the impl, and we already know that adding new opt out traits is a breaking change, just like adding opt out constness would be.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Second, ~const bounds are new weird syntax. And, for many, Rust already has a lot of weird syntax, so we should be wary about adding more. On the contrary, users are already used to ?Sized bounds, so ?const bounds is not that weird or different. I am very unconvinced by argument below that this ~const syntax is "the one folks are used to for a few years now":

I didn't say ~const is what they are used to on nightly, but I see the ambiguity. What I meant was folks on stable are used to T: Trait bounds existing and giving you only static access to the trait items.

I'll adjust the text

Copy link
Member

@fee1-dead fee1-dead Jan 15, 2025

Choose a reason for hiding this comment

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

I argued against using ?const for implicit maybe-const, and opting out for non-const bounds semantics in my comment in the Zulip thread which was mentioned above.

~const reads to me very strongly as "not" or something along those lines, since ~ is bitwise not in C/++ and indicates a destructor in C++, and I think this is likely to be more understood by users than ~ meaning maybe. Not that we should necessarily base any syntax decisions off of C/++, but I don't know that a percentage of users being familiar with a nightly syntax makes that strong of a case either.

I'd say that ?const reads much more strongly as "not" for me, as with ?Sized, so I don't like the idea of using ?const as syntax of what ~const does today. I mainly oppose this because of the dissonance implied in that: adding ?Sized opts-out and relaxes requirements. Suggesting that ?const (used in place of ~const) would "opt-out" of traits being non-const by default is a stretch, especially since it doesn't relax requirements, it makes requirements stricter.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, good point. When we see a fn type anywhere outside a function signature we can't tell if this is ever meant to be used in const contexts.

Maybe the defaults should be different for static and dynamic dispatch (the latter being fn() and dyn Trait types), but that could also easily be confusing. The RFC says next to nothing about the vision for dynamic dispatch so it's hard to compare.

Copy link
Contributor

Choose a reason for hiding this comment

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

(in reply to #3762 (comment))

context-dependent syntax

With your proposal, in a normal fn, : Trait just means “requires Trait”, but in a const fn, it means “requires Trait in non-const context, and const Trait in a const context”. The context of fn vs const fn affects the meaning of the construct.

Copy link
Member

@RalfJung RalfJung Jan 19, 2025

Choose a reason for hiding this comment

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

You already said that above, and I already answered it: The meaning of T: Trait under my proposal is always "must be const in const context". Some functions just cannot be called in const context, making the requirement equivalent to "doesn't have to be const". I see no reason to explain this in such a complicated way as you did, and I disagree with the claim that this is context-dependent. We just have the simple rule that the bound, by default, matches the const context (i.e. behaves like ~const) except when you want to change it by saying "must always be const" or "doesn't ever have to be const", which are the less common cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

less common cases

I don’t think it will be all that uncommon, because of marker traits with no const version. And I think e.g. ?const Copy is far more likely to cause confusion than anything in this RFC’s syntax, on top of being a strict downgrade from the current edition.

Copy link
Member

Choose a reason for hiding this comment

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

The RFC says next to nothing about the vision for dynamic dispatch so it's hard to compare.

My personal thinking is that fn pointers intended to be used in const contexts should be ~const fn, and the same goes for dyn ~const Fn(). That to me makes it much more consistent, as compared to the opt-out which might require a split. That is one of the reasons I support ~const (or any syntax for opt-in) over ?const (or any syntax for opt-out).

@bushrat011899

This comment was marked as duplicate.

@compiler-errors

This comment was marked as duplicate.

@oli-obk oli-obk added the A-const-eval Proposals relating to compile time evaluation (CTFE). label Jan 14, 2025
which we definitely do not support and have historically rejected over and over again.


### `~const Destruct` trait
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this should just be ~const Drop? Drop bounds in their present form are completely useless, so repurposing them would make sense. Drop would be implemented by every currently existing type, and ~const Drop only by ones that can be dropped in const contexts.

(Overall, very impressed by this RFC. It addresses essentially all the concerns I thought I might have going in. Thank you @oli-obk and team for all your hard work!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea that would be neat. But it needs an edition and giving the ppl that needed T: Drop bounds the ability to still do whatever they were doing

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 15, 2025

Choose a reason for hiding this comment

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

the ppl that needed T: Drop bounds

Are there any such people at all? Making more types implement a trait should not be breaking or require an edition, no? Unless there is some useful property (for e.g. unsafe code) that only types that are currently Drop have—and there isn’t, AFAICT. (Plus, removing an explicit Drop impl from a type is usually not considered breaking.)

Copy link
Member

@compiler-errors compiler-errors Jan 15, 2025

Choose a reason for hiding this comment

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

I still think it's very useful conceptually to split Destruct and Drop since the former is structural and the latter really isn't -- it's more like an "OnDrop" handler. If we moved to ~const Drop, then in order to write a well-formed ~const Drop impl, you need to write where {all of my fields}: ~const Drop in the where clause.

That is to say, there's a very good reason we split ~const Destruct out of ~const Drop in the first place :)

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 15, 2025

Choose a reason for hiding this comment

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

it's more like an "OnDrop" handler.

Yeah, that’s what it is right now, but we could expand its meaning.

in order to write a well-formed ~const Drop impl, you need to write where {all of my fields}: ~const Drop in the where clause.

The bound could always be made implicitly inferred. Drop is extremely magic already, why not a little more?

But actually, I think it’s a good thing that these bounds can be specified explicitly, because it enables library authors to leave room for adding or changing private fields in the future. I could see allowing impl Drop/impl const Drop blocks with no fn drop() method, that serve only to add restrictions on dropping in const contexts. (In today’s Rust, you could use a ZST field for this.)

Copy link
Member

Choose a reason for hiding this comment

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

It would still be possible to change the meaning of : Drop over an edition

Right, so before we have a consensus on how that would look like, having both Destruct and Drop feels completely fine for me. Since we just want to know whether something can be dropped (T: ~const Destruct) and we know that we will accommodate any existing uses of the Drop bound and impls, any reformulating of how that works can still be done through an edition even if we choose to add Destruct here.

Copy link
Member

@workingjubilee workingjubilee Jan 17, 2025

Choose a reason for hiding this comment

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

Changing how random trait bounds of otherwise typical traits are presented, even over an edition, is not useful. It's not Fn, FnMut, FnOnce, or Sized. The mistake isn't that you can write a Drop bound, it's that Drop was handled by a typical trait, despite having atypical needs, and was not given special treatment to begin with. That is something you cannot simply change over an edition. Otherwise, introducing a magical special case too-late to help is not really for the best.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 17, 2025

Choose a reason for hiding this comment

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

@workingjubilee Can you elaborate? To be clear, my suggestion is that Drop should be like a normal trait (at least in terms of its trait bounds). The “magical special case” I suggested would be only for old editions, to preserve compatibility for the small number of people relying on the current not-like-a-normal-trait behavior (where a type that satisfies the Drop bound is less capable than one that does not).

Copy link
Member

Choose a reason for hiding this comment

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

??? Perhaps I misunderstood something?

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 18, 2025

Choose a reason for hiding this comment

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

Current Drop: trait bound satisfied only when an explicit impl exists. Such an impl must contain an fn drop(). Adding such an impl makes the type less capable.

Proposed Drop: trait bound always satisfied on new editions (like this RFC’s Destruct). Bare : Drop bounds retain their current behavior on old editions (with a warning), for compatibility. ~const Drop bounds behave like this RFC’s ~const Destruct on all editions. Conceptually: when implementing Drop manually, you override the default impl (like with an auto trait). An explicit impl may specify ~const bounds, or an fn drop() handler. Adding such a handler implicitly (a) makes the type ineligible for destructuring, and (b) unimplements auto trait TrivialDrop.

text/0000-const-trait-impls.md Show resolved Hide resolved
Since it's only necessary for a transition period while a crate wants to support both pre-const-trait Rust and
newer Rust versions, this doesn't seem too bad. With a MSRV bump the proc macro usage can be removed again.

## Can't have const methods and nonconst methods on the same trait
Copy link

@matthieu-m matthieu-m Jan 16, 2025

Choose a reason for hiding this comment

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

How difficult would it be to support const associated functions in traits from the get go?

trait Foo {
    const fn ctfe();
    
    fn runtime();
}

There's already significant support for const functions today:

  • Functions can be declared const, and invoked at compile-time.
  • const trait functions can be invoked at compile-time.

As an observer it seems a bit bizarre to me to launch const traits without support for const associated functions, which is likely to lead to churn in the ecosystem, rather than "close the gap" first.

But being just an observer, maybe I'm just misunderstanding how much work there would be to bring const associated functions?

Note: I do understand we don't have them today, I merely think the RFC could perhaps take the stance they should be implemented before stabilizing const Trait, it'll take a while anyway, so hopefully they would be!

Copy link
Member

Choose a reason for hiding this comment

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

As an observer it seems a bit bizarre to me to launch const traits without support for const associated functions, which is likely to lead to churn in the ecosystem, rather than "close the gap" first.

Which primary use case do you envision allowing const fn in traits enable?

Choose a reason for hiding this comment

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

As an example, consider the Store proposal, which today implements the split-trait strategy in order to provide a const-constructible dangling handle, without forcing the user to have a fully const Store, because calling OS primitives isn't const:

#[const_trait]
trait StoreHandle {
    type Handle: Copy + ...;

    fn dangling(&self) -> Self::Handle;
}

#[const_trait]
trait StoreSingle: StoreHandle {
    fn allocate(&mut self, layout: Layout) -> Result<Self::Handle, AllocError>;

    ...
}

This is necessary for:

impl<T, S> Vec<T, S>
where
    S: StoreSingle,
{
    pub const fn new() -> Self
    where
         S: ~const StoreHandle
    {
         todo!()
    }
}

The ideal interface would instead be:

const trait StoreSingle {
    type Handle: Copy + ...;

    //  No reason NOT to have a const constructible handle type.
    const fn dangling(&self) -> Self::Handle;

    fn allocate(&mut self, layout: Layout) -> Result<Self::Handle, AllocError>;
}

Which makes for a simpler API, and enables a simpler implementation:

impl<T, S> Vec<T, S>
where
    S: StoreSingle,
{
    pub const fn new() -> Self {
         todo!()
    }
}

Is a simpler API sufficient motivation to wait for const associated function in traits? Or should the current trait API be split, knowing that splitting is a breaking change, and so is fusing them back when const associated functions make it to stable?

I am afraid that this is the kind of unfortunate trade-off that library maintainers will face, with users that don't care about the const asking to wait, and users who do care about const asking to make the two breaking changes.

It's an uncomfortable situation.

Copy link
Member

Choose a reason for hiding this comment

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

I do think this should be looked at, designed, and implemented. However I disagree with saying that we should prioritize this before const traits. Almost all traits in the standard library would benefit from having const traits, and the standard library currently does not have traits (or at least I don't know of anything) that requires all implementers to write a const fn. It makes much more sense for us to try to get for x in 0..100 {} in const working, and then see if this design pattern would be beneficial.

Also, it would be helpful if you could elaborate (perhaps with more context) on why an associated const wouldn't work in this case, though my point above still stands.

Choose a reason for hiding this comment

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

Also, it would be helpful if you could elaborate (perhaps with more context) on why an associated const wouldn't work in this case, though my point above still stands.

It's a good question, actually. I would expect in most cases S::DANGLING would work quite well. The one tiny advantage of store.dangling() is that the return value can be depend on the instance of store. For example it may allow "randomization" by passing a different seed to store, to help fuzz usecases where dangling handle is incorrect used to ultimately produce a reference. Quite niche, admittedly.

It makes much more sense for us to try to get for x in 0..100 {} in const working, and then see if this design pattern would be beneficial.

Oh I definitely wish to be able to use traits in const contexts. And for loops in particular.

Which is why I asked how much work it would be to support const associated functions.

It seems like most of the scaffolding is here to me, but I am unfamiliar with compiler internals. If the answer is "it's a couple days work", then I'd argue it's really worth it to avoid all the potential churn (and maintainer pains) in the ecosystem. If the answer is "it's at least a month work, possibly a lot more as there are unresolved questions", then I'll back the decision to just stabilize const traits first without reserve.

Would you have an educated guess as to the amount of work required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the implementation effort is small, and waiting for the feature doesn't unduly delay the stabilization of this RFC, then it seems to be an obvious shoe-in to me: a small effort, for large potential savings.

I think the impl effort is not too large. But we'll still end up with surface language that may be super rarely used.

Once an API performs its intended purpose, a maintainer may -- rightfully -- be unwilling to change the API just to "clean it up". In fact, even if the library undergoes a major release anyway, the maintainer may still be reluctant to remove the work-around if they judge that doing so will require too much -- for too little gain -- on their own & their users' parts.

Yea. This is a problem we're thinking about with marking just some methods as conditionally const, too. So if we do always const methods, then we should do conditionally const ones, too. And const RPIT, because ppl will write structs instead of RPITs if they can't use RPITs. I'm not sure what the extensions are that we need that will avoid crate authors building slightly unusual APIs that they'd want to have native Rust support for.

And this is where one may rightfully ask why not use RTN instead of all of these individual patches around what RTN would just allow. Ignoring my opinions on RTN and the impl effort required for it, we'd then need to figure out what kind of problems that produces for API authors and users.

Sorry I don't have any answers and only concerns, you may be entirely right at where you draw the line of what is worth doing, but I cannot tell. Possibly just because I have been doing stuff around const traits for years and like the simplicity we've reached right now.

Copy link
Member

@programmerjake programmerjake Jan 20, 2025

Choose a reason for hiding this comment

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

And this is where one may rightfully ask why not use RTN instead of all of these individual patches around what RTN would just allow. Ignoring my opinions on RTN and the impl effort required for it, we'd then need to figure out what kind of problems that produces for API authors and users.

I have some objections to using RTN as a replacement for marking some trait methods as const:

  1. if we have a RTN-like syntax, it should not be T::method(..): const since that looks like marking the return type as const rather than the method itself, I think syntax like T::method: const or T::fn method: const is better RTN-like syntax (there should be a better name than RTN since RTN is Return Type Notation -- syntax for getting the return type, not the method itself).
  2. using RTN-like syntax makes trait bounds extremely verbose, e.g.
    where
        T: MyTrait,
        T::foo: ~const,
        T::bar: ~const,
        T::baz: ~const,

To be clear, I'm not saying RTN-like syntax shouldn't exist, but that it should only be used where you can't just mark some trait methods as const in the trait definition or mark the whole trait as const.

Copy link
Contributor

@traviscross traviscross Jan 21, 2025

Choose a reason for hiding this comment

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

This potential for verbosity is shared with RTN also, and the answer we settled on in that case is that we would ship trait aliases, so that rather than repeating, e.g.,

where
    T: MyTrait,
    T::foo(..): Send,
    T::bar(..): Send,
    T::baz(..): Send,

...that either the trait author or a downstream user could write a trait alias such as trait SendMyTrait = .. that states all those bounds once, and that in fact the trait author could use a proc macro (provided by our project) to wrap that up and never have to spell out those bounds at all, e.g.:

#[trait_variant::make(SendMyTrait: Send)]
trait MyTrait { .. }

In fact, that already works today without trait aliases, because we can polyfill that in other ways.

The point is, shipping some more precise and expressive mechanism doesn't mean that people are forced to verbosely repeat themselves. As with RTN, we can build on top of it to handle the common case.

In terms of a name, I'd probably boringly call this return effects notation (REN). In terms of syntax, I don't think T::foo: .. is right. That notates the type of the function item itself, which the set of output effects is not. If we had or planned a generic effects notation, e.g.:

fn f<effect K>(x: u8) -> u8 do K { x }
//   ~~~~~~~~         ~~~~~ ~~~~
// Generic effect       |      |
//   parameter     Return type |
//                             |
//                       Output effects

Then I'd perhaps suggest to mirror that with REN, directly extending RTN in a similar syntactic way, so we'd write, e.g.:

where T: Tr<foo(..): Send do const>

(There are other reasons that may make an RTN-style approach difficult in this case, but syntax or verbosity don't strike me as the blockers any more than they did for RTN.)

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 21, 2025

Choose a reason for hiding this comment

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

I agree about the parallel to trait aliases, it motivates my proposal here.

output effects

What do you mean by “output” here?

Copy link
Contributor

@traviscross traviscross Jan 21, 2025

Choose a reason for hiding this comment

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

If we were to write:

fn f<T: Send>() -> Vec<T> { Vec::new() }
//   ~             ~~~~~~
// Input type    Output type

We might say that the function takes an input type and produces an output (or return) type. Similarly, with generic effects, we could say:

fn f<effect K>() -> () do K - const { .. }
//          ~             ~~~~~~~~~
//   Input effect set     Output effect set

That is, the function takes an input set of effects, and produces an output (or return) set of effects.

A full example how how things would look then

```rust
const trait Foo: Bar + ?const Baz {}
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be an equivalent of this example with the current proposed syntax (const trait Foo: ~const Bar + Baz {}), just to be clear and explicit about what supertrait bounds look like.

Ideally, there would also be an example with #![feature(trait_alias)] (e.g. const trait Foo = ~const Bar + Baz;); or alternately, those should be explicitly relegated to a future possibility.

Copy link
Member

@RalfJung RalfJung left a comment

Choose a reason for hiding this comment

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

Overall I am really excited about this; with &mut out of the door, traits are the next big frontier for const. I fully agree we shouldn't block this on the async/try effects work; const is quite different since there's no monadic type in the language reifying the effect, and I also don't want to wait another 4 years before const fn can finally use basic language features such as traits.

My main concern is the amount of ~const people will have to add everywhere. I'm not convinced it's such a bad idea to make that the default mode for const fn. However that would clearly need an edition migration so it doesn't have to be part of the MVP. I just don't agree with the way the RFC dismisses this alternative.

text/0000-const-trait-impls.md Show resolved Hide resolved
Comment on lines +498 to +499
Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere.
We may offer an opt out of this behaviour in the future, if there are convincing real world use cases.
Copy link
Member

Choose a reason for hiding this comment

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

Interesting. This can be seen as an extension of the fact that all vtables have a drop slot, so in a sense all traits already have the Destruct supertrait.

{
...
}
```
Copy link
Member

Choose a reason for hiding this comment

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

For the reference-level section, this seems more understandable than the <T as Default>::k#host = Conditionally thing above, but maybe that's just because I have already through about the "be generic over constness" formulation quite a bit.

text/0000-const-trait-impls.md Outdated Show resolved Hide resolved
body to compile and thus requiring as little as possible from their callers,
* ensuring our implementation is correct by default.

The implementation correctness argument is partially due to our history with `?const` (see https://github.com/rust-lang/rust/issues/83452 for where we got it wrong and thus decided to stop using opt-out), and partially with our history with `?` bounds not being great either (https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209). An opt-in is much easier to make sound and keep sound.
Copy link
Member

Choose a reason for hiding this comment

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

The first issue referenced here is not a ?const issue, it is a min const fn issue, isn't it? We meant to reject const fn foo<T: Foo>() but some loopholes were left open.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, which is my whole argument. An opt out is too easy to get wrong.

Copy link
Member

Choose a reason for hiding this comment

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

The text says this is about the history of ?const bounds, and then links to a PR that has nothing to do with ?const bounds. That's at the very least confusing.

I also find this not a good justification for lang design decisions -- a borrowck is also easy to get wrong, so should we just not do it? Or a coherent trait system? IMO this is not a good argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the behavior is hard to understand for implementers, it’s going to be even harder for users. (Borrowck is a great example here!) Given that the simpler design can do anything the complicated one can, why bother with the latter?

Copy link
Member

@RalfJung RalfJung Jan 19, 2025

Choose a reason for hiding this comment

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

The implementation concerns aren't about "hard to understand". The concept of "const fn should not have trait bounds" is trivial to understand, it just turns out it's easy to get wrong during implementation.

Given that the proposal differs from the RFC only in syntax, I don't think the proposed alternative is any harder or easier to understand than what the RFC says. The concepts people have to understand (and the concepts that have to be implemented in the compiler) are the exact same either way.

There are good arguments against making ~const Trait the default. I think the RFC should focus on gathering those. I just don't think "we got something wrong in the compiler because it was implemented in a very syntax-directed manner" is a good argument.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 19, 2025

Choose a reason for hiding this comment

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

only in syntax

Yes, syntax that is context- and edition-dependent. That’s harder both for implementers and for users. The current RFC does not have that problem.

Copy link
Member

@RalfJung RalfJung Jan 19, 2025

Choose a reason for hiding this comment

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

That's a different argument from what the RFC makes, so it seems you are agreeing with me on the original point that started this subthread?

Not sure what you mean by "context-dependent syntax". But yes, an edition migration has downsides. Anyway please move discussion of the alternative proposal to the other thread; this thread is about the paragraph starting "The implementation correctness argument is partially due to our history". The argument that the RFC makes here is rather odd in two ways:

  • The argument is not supported by the issue it links. I don't think "we got something entirely unrelated to ~const wrong in the past" supports the claim that ?const is hard to implement. I don't buy the relation to ?Sized either, these are very different things -- ?Sized adds a fundamentally new concept to the language that would not exist otherwise; ?const does not involve a new concept compared with ~const.
  • It's a weak argument to begin with, given that the entire underlying complexity here is about parsing. "We forgot to check where clauses" is a mistake that just sometimes happens; I don't buy the implicit claim that the proposed RFC is somehow immune to oversights like this (or less susceptible to them than the ?const alternative). The ?Sized issues arise because rustc architecture makes it somewhat tricky to support certain syntax only in a few specific places rather than everywhere. The obvious solution is to support the syntax everywhere. ;) More seriously though
    • ~const apparently would also be supported only in a few places, so it has exactly the same issue.
    • If the syntax provides enough benefits, I don't think we should reject it for reasons like this. That would be really, really bad news for the future evolution of Rust. Maybe we should block it on a re-architecture of the parser that makes such issues less likely to occur, or so, but we can't stop adding good syntax just because our parser/lowering has a suboptimal architectures.

Copy link
Contributor Author

@oli-obk oli-obk Jan 20, 2025

Choose a reason for hiding this comment

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

  • ~const apparently would also be supported only in a few places, so it has exactly the same issue.

The linked things about other ? bounds like ?Sized or abitrary ?Trait are not parser issues. They are handled "correctly" all the way to the type system, they are just either redundant there, because the traits are already opted out, or in the case of ?const we were missing a requirement for an opt-out. It is much easier to forbid an opt-in, instead of requiring an opt-out, because you're forbidding the existance of something instead of forbidding the non-existance of something.

Anyway, I do not want to review, maintain or do that work (and thus be on the hook for any potential breakage around there), so 🤷 I will not talk about it anymore and others can figure it out. We can instead talk about the lang reasons not to do the opt out (other threads).

If the syntax provides enough benefits, I don't think we should reject it for reasons like this.

Agreed, but I consider this another nail in the coffin of ?const, which I don't think we should have out of lang reasons

text/0000-const-trait-impls.md Outdated Show resolved Hide resolved
text/0000-const-trait-impls.md Show resolved Hide resolved
text/0000-const-trait-impls.md Show resolved Hide resolved
@Aloso
Copy link

Aloso commented Jan 18, 2025

The RFC doesn't mention which traits in the standard library will be constified. I guess the goal is to constify all of them, but it might not be possible to constify traits that use unsupported features, e.g. heap allocation.

This is due to the main limitation of this proposal: To constify a trait, all methods have to be const. This also means that marking a trait as const is a big comment, since after that you can't add non-const default methods backwards compatibly.

I particularly care about Iterator, which has a lot of default methods, and has a big impact due to its use in for loops. I'd like to know if Iterator can be constified under this proposal.

@onestacked
Copy link

onestacked commented Jan 18, 2025

@Aloso Iterator should be possible with this RFC, however with how many methods it has that is quite difficult to do all at once.

AFAIK the current plan for constifying Iterator is to add a way to add a way to declare some trait methods as always non const with something like a rustc_non_const_trait_method attribute (see this issue). Not sure if that will be usable by non std code.

Deciding what exactly to constify in std is a Libs-API question and not really in scope for this PR.

@oli-obk oli-obk force-pushed the const-trait-impl branch 2 times, most recently from be2ea15 to efc6bb1 Compare January 20, 2025 10:05
Comment on lines +829 to +853
## Per-method and per-trait constness together:

To get the advantages of the per-method constness alternative above, while avoiding the new kind of breaking change, we can require per-method and per-trait constness:

A mixed version of the above could be

```rust
const trait Foo {
const fn foo();
fn bar();
}
```

where you still need to annotate the trait, but also annotate the const methods.

But it makes it much harder/more confusing to add

```rust
trait Tr {
const C: u8 = Self::f();
const fn f() -> u8;
}
```

later, where even non-const traits can have const methods, that all impls must implement as a const fn.
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 20, 2025

Choose a reason for hiding this comment

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

Another possible syntax is:

const trait Tr {
    // Must be `const` in all impls
    const fn foo();
    // Must be `const` in `const` impls
    ~const fn bar();
    // Never needs to be `const`
    fn baz();
}

This is analogous to the syntax for trait bounds.

A further possibility is to not require the const keyword in front of the trait for impls, but instead infer based on the annotations of the methods:

// This impl is `const`, because `fn bar()` is
impl trait Tr for () {
    const fn foo() {}
    const fn bar() {}
    fn baz() {}
}

// This impl is not `const`, because `fn bar()` is not
impl trait Tr for u32 {
    const fn foo() {}
    fn bar() {}
    fn baz() {}
}

This has several advantages:

  • Implementations of functions can run in const if and only if they are annotated as const in the source.
  • If combined with allowing impls to individually refine any function as const: impls can start marking functions as const immediately after the feature stabilizes, without waiting for the library defining the trait to update. And when said library does eventually update their trait to const Trait, the implementers will automatically support it without need for further changes.

It also imposes the limitation that a const trait must contain at least one ~const function or bound. That is not necessarily a bad thing, however.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems very suprising to have such aggregate behaviour where individual method annotations change sth about the entire impl.

We have that for fields, but there's no precedent for impls and it seems a footgun

Copy link
Member

Choose a reason for hiding this comment

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

Depends how we interpret what const Trait means; we could mean it as "the methods can be called in a const context". This would solve the ~const Copy issues because that would make T: const Copy and T: Copy equivalent.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jan 20, 2025

Choose a reason for hiding this comment

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

In my proposal above, const Trait bounds are basically sugar for the RTN-like syntax discussed at e.g. #3762 (comment). In my example above, bounding by const Tr is equivalent to defining a trait alias trait ConstTr = Tr where Self::bar: const;, and then bounding by ConstTr.

In other words, const Trait is nothing more than a convenient alias for “implements Trait with certain functions being const”. It’s a derived property of the impl, not a fundamental one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Arguably “it’s just an alias” also makes the feature easier to teach.

It also imposes the limitation that a const trait must contain at least one ~const function or bound.

Alternatively, we could allow it with a warning, so that a const Trait bound for a const trait with no ~const in its definition is equivalent to the bare Trait bound.

- Feature Name: `const_trait_methods`
- Start Date: 2024-12-13
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#67792](https://github.com/rust-lang/rust/issues/67792)
Copy link
Member

Choose a reason for hiding this comment

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

this should probably be changed to a new tracking issue, see: rust-lang/rust#67792 (comment)

Comment on lines +548 to +549
Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere.
We may offer an opt out of this behaviour in the future, if there are convincing real world use cases.
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't that prevent const-ification of standard library traits? I.e. if we stabilize that (some) standard library traits imply ~const Destruct, we won't be able to take that back.

It's also quite easy to come up with an example where we'd want a const trait but not ~const Destruct:

static EV: Vec<u8> = Vec::new(); // this works
static EV: Vec<u8> = Vec::default(); // so why shouldn't this?

Copy link
Member

Choose a reason for hiding this comment

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

Some good example traits that I don't think should ever imply ~const Destruct: AsRef, AsMut, Borrow, Deref, PartialEq, ToString, fmt::Debug (and other formatting traits, though fmt::Write is probably an exception)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-const-eval Proposals relating to compile time evaluation (CTFE). 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.