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 16 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.

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

@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 (RKN). 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 RKN, 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. The output set of effects are the effects that may be exhibited by the function itself.

Copy link
Member

@RalfJung RalfJung Feb 3, 2025

Choose a reason for hiding this comment

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

"output effects" or "return effects" make no sense to me. In formal notation, the effects of a function are often annotated at the arrow, since they are part of the process from the inputs to the outputs. Also why do you abbreviate "effects" with "K" in "RKN"?

What you call "input effect set" and "output effect set" is an artifact of row polymorphism, I don't think it s a good guide for terminology here.

I'd call this something like "effect bounds" or so; it is about controlling the effects of trait functions.

Copy link
Contributor

@traviscross traviscross Mar 13, 2025

Choose a reason for hiding this comment

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

Also why do you abbreviate "effects" with "K" in "RKN"?

It reads better than "REN" to me, and it's common to want to replace vowels with consonants in these kind of initialisms (e.g. using "X" to represent "express"). Using "K" for "effect" seems natural and something people do, e.g. in the "Effekt" language, or in our own lang discussions where in playing with whether to avoid using the word "effect", we started using "Ks" as a placeholder for "effects", and we've often used K as the default name for an effect parameter in the same way that T is the default name for a type one.

There is one thing I find unfortunate about using "K" here, which is that we (the PL community broadly) also tend to use "k" for "continuation".

"output effects" or "return effects" make no sense to me...

Yes, I don't know. I had gone back and forth about it and decided I thought it did make sense in the following way. Given:

trait Tr<T> {
    type Ty;
}

We might (and tend to) say that this takes a set of input generics and produces an "output" type. It's a type-level mapping.

So, if we had the effect equivalents of this:

trait Tr<effect K> {
    effect Kt;
}

Then here too, I'd probably want to say that we have input and output effects.

So, then, in the same way that we map functions to Fn* traits with input and output type generics, probably we'd map effectful functions to Fn* traits with input and output effect generics.

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.

{
...
}
```
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.

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

Copy link
Member

Choose a reason for hiding this comment

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

I'm completely okay with the RFC comparing ~const and ?const entirely based on lang reasons and concluding that ~const is the better choice for now. In fact I think that's exactly what the RFC should do -- the migration effort alone makes ?const not suited for an MVP. But then the text that this thread is attached to (i.e., the implementation reasons) should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know how to remove that without also removing the history. Feel free to propose a diff and I'll accept it. Otherwise I will leave it as is. Everything except these two sentences is about the non-impl things.

Copy link
Member

Choose a reason for hiding this comment

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

I still think "An opt-in is much easier to make sound and keep sound" is just not true (the difference is entirely syntax). And the issue linked here

due to our history with ?const (see rust-lang/rust#83452 for where we got it wrong and thus decided to stop using opt-out)

is not about ?const. So my proposed edit would be to just remove this paragraph. Since I assume you won't like that I'll add a proposal that just removes the IMO entirely unrelated points about ?Sized.

Comment on lines +759 to +760
Such an ecosystem would also make `const fn` obsolete, as every `const fn` can in theory be represented
as a trait, it would just be very different to use from normal rust code and not really allow nice abstractions to be built.
Copy link
Contributor

Choose a reason for hiding this comment

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

const generics has limitations, so no, it would not make const fn obsolete…

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Redundant" may be the better word. And yes, we'd need more const generics features

Comment on lines 360 to 375
Arguments and the return type of such functions and bounds follow the same rules as
their non-const equivalents, so you may have to add `~const` bounds to other generic
parameters, too:


```rust
const fn foo<T: ~const Debug, F: ~const Fn(T)>(f: F, arg: T) {
f(arg)
}

const fn bar<T: ~const Debug>(arg: T) {
foo(baz, arg)
}

const fn baz<T: ~const Debug>() {}
```
Copy link
Contributor

@traviscross traviscross Feb 6, 2025

Choose a reason for hiding this comment

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

I was interested in the question of in what cases the ~const in foo<T: ~const Debug, ..> might be needed. Here's a mockup to check my understanding:

Playground link

That is, I gather that it's not needed, which is I suppose not surprising, though a bit subtle, since the Debug bound isn't needed there either.

If that encoding is wrong, of course, let me know; otherwise perhaps it'd be more clear to write this example as const fn foo<T, F: ~const Fn(T)>.

While that can in generic contexts always be handled by adding more `~const Destruct` bounds, it would be more similar to how normal `dyn` safety
works if there were implicit `~const Destruct` bounds for (most?) `~const Trait` bounds.

Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere.
Copy link
Contributor

Choose a reason for hiding this comment

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

In working through some examples, I find that the other place that ~const Destruct bounds are needed rather pervasively is on associated types. E.g. (simplified):

const trait FnOnce<Arg> {
    type Output: ~const Destruct;
    fn call_once(self, arg: Arg) -> Self::Output;
}

The idea, I gather, is to leave these to be written explicitly?

Copy link
Member

@programmerjake programmerjake Feb 7, 2025

Choose a reason for hiding this comment

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

I think a possible solution to this and to the issue that many traits don't need a ~const Destruct super trait is to operate the same way that having a type &'a T implicitly adds a T: 'a bound: automatically add ~const Destruct bounds on all types that are function argument/return types. so, e.g.:

pub const trait AsRef<T: ?Sized> { // no implicit `~const Destruct` bound
    // `&self` is an argument, so add `for<'a> &'a Self: ~const Destruct` -- which does nothing
    // `&T` is a return type, so add `for<'a> &'a T: ~const Destruct` -- which does nothing
    fn as_ref(&self) -> &T;
}
pub const trait Default {
    fn default() -> Self; // `Self` is a function's return type, so add `Self: ~const Destruct`
}
pub const trait FnOnce<Arg> {
    type Output;
    // `self` is an argument, so add `Self: ~const Destruct`
    // `arg` is an argument, so add `Arg: ~const Destruct`
    // `Self::Output` is a return type, so add `Self::Output: ~const Destruct`.
    fn call_once(self, arg: Arg) -> Self::Output;
}

I'm not sure if those bounds should be added to the trait, or merely to the trait's methods, or some combination of that. There should probably be a way to opt-out of adding implicit bounds.

Copy link
Member

Choose a reason for hiding this comment

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

dyn Trait could also have ~const Destruct bounds deduced, inspired by how dyn Trait lifetime deduction works:
&dyn Trait and &mut dyn Trait both default to not having ~const Destruct bounds (similar to how they default to having the same lifetime as the reference), other dyn Trait default to having ~const Destruct bounds (similar to how it defaults to being 'static).

@oli-obk
Copy link
Contributor Author

oli-obk commented Mar 7, 2025

The design has pivoted to replacing ~const with (const) and no more impl const Trait for Type or const Trait, instead the presence of const or (const) methods makes that implicit. See also https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/.22constantly.20effective.22/near/503902077 for background on where this change came from.


It is now allowed to prefix a trait name in an impl block with `const`, marking that this `impl`'s type is now allowed to
have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like usual, but more on this later).
Traits can declare methods as `const`. Doing so is a breaking change, as all impls are now required to provide a `const` method,
Copy link
Member

Choose a reason for hiding this comment

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

adding const to an existing method is a breaking change.

Copy link
Contributor

Choose a reason for hiding this comment

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

Removing it also breaking, no? As consumers can no longer rely on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the point is adding a new defaulted function is fine to be const. it's when you add const to an existing function


### Conditionally const traits methods

Traits need to opt-in to allowing their impls to have const methods. Thus you need to prefix the methods you want to be const callable with `(const)`:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think (const) is a step-down from ~const.

type Foo = (i32); is equivalent to type Foo = i32; and let x = (7) is the same as let x = 7—there is a prior expectation that freestanding parentheses are just for grouping and don’t change the meaning of the contained construct. (const) breaks this expectation, which is misleading and confusing for users.

In contrast, ~ has a natural connotation of “maybe” or “sort of”, which is quite close to the concept being conveyed. It’s a natural fit. In my view, users having to learn a new sigil is preferable to users having to learn a confusing new overloaded meaning for an existing sigil.

Copy link
Contributor Author

@oli-obk oli-obk Mar 7, 2025

Choose a reason for hiding this comment

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

Quoting myself

I'm gonna die on the hill that I support whatever gets landed faster

It's just painting the shed now

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 9, 2025

Choose a reason for hiding this comment

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

Also: parentheses-around-const has another potential meaning.

Consider this function:

const fn foo<T: Bar>(x: T) -> impl Baz {
    /* ... */
}

Imagine we wanted to specify that the return type should be const Baz if and only if T: const Bar. One potential syntax would be:

const fn foo<T: Bar>(x: T) -> impl (const where T: const Bar) Baz {
    /* ... */
}

It would be a shame to paint ourselves into a corner here!

This comment was marked as outdated.

Copy link
Contributor

@traviscross traviscross Mar 9, 2025

Choose a reason for hiding this comment

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

In contrast, ~ has a natural connotation of “maybe” or “sort of”...

For me, depending on context, ~ either means "not" or "approximately", and I think of "approximately" and "maybe" (in the sense meant here) as being rather different. (We do use the word "maybe" to mean "approximately" sometimes, informally, e.g. in "it happened maybe ten thousand years ago", but that's not the sense of the word "maybe" that we're interested in.)

If we had nothing else, of the limited options available on the keyboard, then maybe we could lean into that pun, but it was never a perfect fit, in my view.

The intuition for parentheses is that we say things like "as always, the software will definitely (not) ship on time," or "if we see an increase (decrease) in the metric, we'll relax (strengthen) our spending controls". That is, we use the parens when we mean something to be read in either of two ways. That fits pretty well with what we're trying to express here.

(As we can see in that second example there, we sometimes tie the parentheses together, such that they're all meant to be toggled as a unit, which is something else that we're doing here.)

Copy link
Member

@RalfJung RalfJung Mar 14, 2025

Choose a reason for hiding this comment

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

I'm sorry you completely lost me here. How does the "item-level syntax space" tie anything to "the associated effect" (and associated with what)? How does the analogy "hold in the item space", what even is the "item space"?

We seem to agree that the analogy does not hold in

const fn default<T: (const) Default>() -> T

which for me means the analogy is dead in the water. Any attempt at teaching const traits with this analogy will cause more confusion than it helps; it will take more time to explain those exceptions that the time you might save by introducing the analogy in the first place. You can make any curved line seem straight by putting on a particularly shaped lens but that doesn't mean it is a valid claim to say that the line is straight...

Copy link
Member

@RalfJung RalfJung Mar 14, 2025

Choose a reason for hiding this comment

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

Thinking about it some more, maybe you are talking about making the following expressible with the syntax?

trait Tr {
  const fn foo<T: (const) Default>() -> T;
  (const) fn bar(i32) -> i32;
}

That is, we can have a function foo where all trait impls must ensure that it is const-callable if its trait bounds are const-satisfied. If we wrote (const) fn here that would mean only const impls have to ensure it is const-callable.

I'm not sure if anyone asked for such functions to exist. The alternative would be to entirely dispense with the idea that we can have always-const functions in a trait. Instead, we can only have sometimes-const functions, i.e., tied to the associated effect, and then leave it up to the user of the trait to bound the associated effect as they see fit.

Copy link
Member

Choose a reason for hiding this comment

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

Put differently: for a situation like the above, where we have two generic effects in scope (the associated effect of the trait, and the one that foo is generic over), I think we shouldn't try to shoehorn that into a "simple" syntax that leaves the effect generics implicit. Once there are two effect generics in scope, we should give them names and make things explicit; that will be a lot easier to explain & understand. We don't need the MVP to support this feature either, we can leave this as a possible future extension.

Copy link
Contributor

@traviscross traviscross Mar 15, 2025

Choose a reason for hiding this comment

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

Yes, this is what it's about. It's what we've been calling "always-maybe-const" vs "maybe-maybe-const", etc. There's a pair of Zulip threads about this in t-lang/effects ("~const fn foo<T: ~const Default>()", "Thoughts on ~const 2024-02-19") that might be worth reviewing for background. Suffice it to say, others feel strongly about spending the syntax in a way that covers all the quadrants here, and I'm also convinced this is the right thing to do.

In terms of teaching this, I do think there's a coherent story here. If we think of the effect of some trait impl method as a union of the effects of other things, what's in that union? Well, to start, all the (const) bounds on the method pull in their default associated effects. Then, if the item is (const), the default associated effect on the trait is added (and if the method is not const at all, then runtime is added). And that default associated effect has in its bounds the associated effects of any (const) bounds from the impl header. Then, on a function elsewhere, a T: (const) Tr bound ties together the effect of that function with the default associated effect of the generic.

That is, the claim was that the parens suggest things are tied together, and they are.

Copy link
Member

@RalfJung RalfJung Mar 16, 2025

Choose a reason for hiding this comment

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

others feel strongly about spending the syntax in a way that covers all the quadrants here, and I'm also convinced this is the right thing to do.

I think the RFC should contain a summary of this rationale. RFCs are supposed to be reasonably self-contained. Having to justify an RFC's choices by pointing at a Zulip thread is not a good sign; it excludes people from the discussion that do not have the time to wade through mountains of Zulip posts.

Also, this doesn't nearly cover all the quadrants, since it still bakes in the idea of a single associated effect per trait. Covering all quadrants would require named associated effects so that I can have multiple groups of functions that are "tied" to each other only within a group, unions of multiple affect variables, etc pp. Apparently the decision was made that that is too much complexity, and I agree, but this proposal is still expending significant amounts of complexity for what I personally think is a feature that does not have to be part of the MVP, and that has a high chance of confusing users.

I understand in this case the PR author isn't the one actually convinced of this design, so ideally those who brought it forward would aid in fleshing out the justification for the RFC document.

In terms of teaching this, I do think there's a coherent story here. If we think of the effect of some trait impl method as a union of the effects of other things, what's in that union? Well, to start, all the (const) bounds on the method pull in their default associated effects. Then, if the item is (const), the default associated effect on the trait is added (and if the method is not const at all, then runtime is added). And that default associated effect has in its bounds the associated effects of any (const) bounds from the impl header. Then, on a function elsewhere, a T: (const) Tr bound ties together the effect of that function with the default associated effect of the generic.

Thanks, this helped a lot to clarify your mental model. The RFC is currently not doing a great job at conveying that. (I didn't the "much more detailed explanation" it references. It's great to have further background, and if I had more free time I'd dig into it, but I think it is reasonable for an RFC to not require such external reading.)

Note that this has two caveats that are actually the same:

* you cannot mark more methods as `(const)` later,
* you must decide whether to make a new method `(const)` or not when adding a new method with a default body.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add an example of a trait with a (const)/~const method, with a default body that calls other trait methods?

Copy link
Contributor

Choose a reason for hiding this comment

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

For example, the full definition of the constified PartialEq trait.

Comment on lines +158 to +162
```rust
trait Trait {
(const) fn thing();
}
```
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 7, 2025

Choose a reason for hiding this comment

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

I appreciate most of the changes made to where const/(const)/~const annotations need to be placed, as they parallel my proposal at #3762 (comment). However, I think requiring the trait definition to say const trait Trait { /* .. */ } was valuable as documentation. One should not have to scroll through the entire list of methods in order to determine whether a trait can be const-qualified. Even though the compiler could infer this, I think users should have to write it out anyway.

(It may also be valuable to allow impl const Trait as an assertion that the impl is const, even if the const keyword is no longer required in that position.)

Copy link
Member

Choose a reason for hiding this comment

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

one other issue with switching to only-methods-get-const is afaict it's incompatible with const Sized as proposed in #3729 since Sized has no methods, but that is a meaningful distinction since size_of's const-ness depends on it.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 7, 2025

Choose a reason for hiding this comment

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

@programmerjake I think the way to resolve that is that size_of() should be made an associated function of Sized.

Choose a reason for hiding this comment

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

Perhaps this is a silly idea, but couldn't size_of() instead be an associated const of Sized? Size is constant, it doesn't change at run time, as far as I am aware. (Adding this and then deprecating size_of() wouldn't be a breaking change, which is an added bonus.)

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 having an associated const would work for the use case of #3729 because it introduces the concept of types that whose size is not known at compile time (but have a consistent size for one execution), so their size can't be put in a const at compile time.

Copy link
Contributor

Choose a reason for hiding this comment

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

for transitional reasons

I disagree with this. I think that, even were this feature to have been introduced prior to 1.0, it would still be valuable to distinguish between traits that can be const-parameterized and ones that can’t. For many traits, having two variants makes no sense. And even when it does make sense, it’s good to not force trait authors to commit until they are ready.

Copy link
Contributor

@traviscross traviscross Mar 10, 2025

Choose a reason for hiding this comment

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

For many traits, having two variants makes no sense

Not to pick too much on one sentence, as I'm sure you understand the underlying model, but I want to highlight it, as this specifically (two variants of a trait) is exactly what we're not doing. And correspondingly, what would concern me about saying const trait is that it would suggest to people that is what's happening, when it's not.

To demonstrate what I mean, this (Tr) is a trait with two variants:

pub trait Tr<T: Tag> {}

pub trait Tag {}
impl Tag for u8 {}
impl Tag for u16 {}

That is, Tr<u8> and Tr<u16> are two variants of the Tr<T> trait. Conversely, this is a trait without any variants:

pub trait Tr {
    type Ty: Tag;
}

Here, there is "only one trait". I.e., the trait itself is not generic; there cannot be any variants of it, and correspondingly any single type can implement this trait only once (i.e. rather than once for each variant).

What we're doing, importantly, in this RFC is the effect equivalent of this second thing, not of the first.

If we were doing the first thing, then I'd agree that const trait or similar would make a lot of sense.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 10, 2025

Choose a reason for hiding this comment

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

I meant “variant” in the same sense that trait-variant uses the term. Again, my mental model is that const Trait desugars to a trait alias, just like SendTrait. (Because trait_alias and return_type_notation are still unstable, trait-variant currently has to emulate them with a second trait. I am referring to the shiny future with no emulation, not the present situation)

Copy link
Contributor

@traviscross traviscross Mar 11, 2025

Choose a reason for hiding this comment

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

Probably I think of T: (const) Tr more as a bound than anything else, like T: Tr<f(..): Tr>. But of course, trait aliases are really just bound aliases, so if we did want to frame it this way, I'd frame it as a (generic) trait alias like this:

trait Tr {
    (const) fn f();
    (const) fn g();
}

// Desugars to:

trait __Tr {
    effect F;
    fn f() do Self::F;
    effect G;
    fn g() do Self::G;
}

trait Tr<effect Effect> = __Tr<F = Effect, G = Effect>
where
    const: Effect; // This is an upper bound.

(This uses the framing of "sets of positive effects" as described in #3762 (comment).)

Then I'd say that we have syntactic sugar that converts T: const Tr to T: Tr<Effect = const> (in the currently-unstable underlying model).

Then, for a trait without any (const) items, we'd desugar trivially as:

trait Tr {
    fn f();
    fn g();
}

// Desugars to:

trait __Tr {
    effect F;
    fn f() do Self::F;
    effect G;
    fn g() do Self::G;
}

trait Tr<effect Effect> = __Tr<F = (), G = ()>
where
    (): Effect; // This is an upper bound.

That upper bound allows traits to migrate -- to configure the associated effects in this trait alias before people start to use const with it. It automatically disallows the T: const Tr sugar, as T: Tr<Effect = const> would not be well-formed (here, I'm assuming invariance).

That upper bound scales nicely if we were to move beyond (const). It's the union of all (thing)s that appear in the trait, which is why it's natural for it to be () when none do.

(Why does this second trait still have associated effects at all? Because it still makes sense to allow impls to "provide more" than the trait definition requires, and to allow these refined impl items to be relied upon when the type is used non-generically.)

Anyway, I hope you can see from this why I don't think there's such a big difference between these two traits -- in this model -- that it's worth overloading const (for const trait) to express this, and why I'm probably pretty happy to just lean on the presence of (const) within the trait or its bounds.

But, you know, opinions differ on this sort of thing. If we did want to mark somehow that the trait had opted-in, other than by the presence of (const), I'd actually prefer to use an attribute or maybe some kind of dummy bound.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 11, 2025

Choose a reason for hiding this comment

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

Thank you for the explanation! IIUC under your model, const trait (or whatever syntax you prefer) would serve as an explicit specification of the upper bound of the effect generic.

There is a parallel here with dyn. There have been proposals in the past for syntax to explicitly assert dyn-compatibility, among them dyn trait Trait { /* ... */ }. If we do ever get a syntax for asserting “dyn Trait is legal” (whatever it looks like), I would expect something similar for “const Trait is legal”. And even if we don’t get such syntax, I would expect rustdoc to surface these two properties similarly.

Of course, there are also differences. dyn is not an effect. And dyn-compatibility is something that is inferred often without the trait author putting any thought into it, while const-parameterization is always explicitly opted in to.

Comment on lines +18 to +20
trait Default {
(const) fn default() -> Self;
}
Copy link
Contributor

@traviscross traviscross Mar 8, 2025

Choose a reason for hiding this comment

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

It's important to highlight somewhere that the intended intuition behind the parentheses in this position is that they explicitly represent what is tied to the associated effect. Following from this, @nikomatsakis and I mean to allow this as well:

impl Default for () {
    () fn default() {}
}

And for that matter:

trait Default {
    () fn default() -> Self;
}

The () says explicitly that the associated effect gets set to the default (e.g. runtime, panic, etc.), so these examples have the same semantics as if the () weren't there.

We're not suggesting that we're necessarily going to encourage this, simply that we want to allow it for consistency and pedagogy. When teaching people, using this form is part of our plan for helping people to really grok what's going on.

So anyway, let's mention somewhere in the RFC that this syntax is allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could lint against it, but disable the lint in macros. Then ppl can write macros that pick const or not easily

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point about it being good for macros.

Comment on lines +481 to +490
### Sites where `(const) Trait` bounds can be used

* `const fn`
* `(const) fn`
* trait impls of traits with `(const)` methods
* NOT in inherent impls, the individual `const fn` need to be annotated instead
* `trait` declarations with `(const)` methods
* super trait bounds
* where bounds
* associated type bounds
Copy link
Contributor

@traviscross traviscross Mar 9, 2025

Choose a reason for hiding this comment

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

Probably we should mention RPIT and APIT specifically here if we mean to allow it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

APIT is just syntax sugar ™️
I'll mention RPIT

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, agreed, RPIT was the main thrust there.

* Allow marking `trait` impls as `const`.
* Allow marking trait bounds as `const` to make methods of them callable in const contexts.

Fully contained example ([Playground of currently working example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2ab8d572c63bcf116b93c632705ddc1b)):
Copy link
Contributor

@traviscross traviscross Mar 9, 2025

Choose a reason for hiding this comment

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

If we mean to allow this with RPIT, which I'd suspect we do, we probably want to add an example here also.

```

and use it in non-generic code.
It is not clear this is doable soundly for generic methods.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
It is not clear this is doable soundly for generic methods.
It is not clear this is doable soundly for generic methods.
## Macro matcher
In the future, we may want to provide a macro matcher for this optional component of a function declaration or trait declaration, similar to `:vis` for an optional visibility. This would allow macros to match it conveniently, and may encourage forwards compatibility with future things in the same category. However, we should not add such a matcher right away, until we have a clearer picture of what else we may add to the same category.

Comment on lines +137 to +138
* `T: const Trait`, requiring any type that `T` is instantiated with to have a trait impl with `const` methods for `Trait`.
* `dyn const Trait`, requiring any type that is unsized to this dyn trait to have a trait impl with `const` methods for `Trait`.
Copy link
Member

@RalfJung RalfJung Mar 11, 2025

Choose a reason for hiding this comment

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

Just to be sure, this means that all methods (those declared fn and const fn in Trait) must be const in the impl?

EDIT: Reading on -- actually this might just refer to all the (const) fn methods in Trait? The order in which it is presented currently makes this somewhat confusing.

Copy link
Contributor

@traviscross traviscross Mar 11, 2025

Choose a reason for hiding this comment

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

  • Given T: Tr, we know that all items declared const in the trait def have a const impl.
  • Given T: const Tr, we additionally know that all items declared (const) in the trait def have a (const) or better impl.
  • Items in the trait declared only as fn cannot be called in const contexts generically.


The `Destruct` marker trait is used to name the previously unnameable drop glue that every type has.
It has no methods, as drop glue is handled entirely by the compiler,
but in theory drop glue could become something one can explicitly call without having to resort to extracting the drop glue function pointer from a `dyn Trait`.
Copy link
Member

Choose a reason for hiding this comment

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

It's already something you can call, isn't it? It's called std::ptr::drop_in_place.

Copy link
Contributor

@traviscross traviscross Mar 12, 2025

Choose a reason for hiding this comment

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

Maybe Destruct should have a method (or associated function) after all.

Comment on lines +626 to +627
Thus we require that all `trait`s with `(const)` methods also have a `(const) Destruct` super trait bound to ensure users don't need to add `(const) Destruct` bounds everywhere.
We may relax this requirement in the future or make it implied.
Copy link
Member

Choose a reason for hiding this comment

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

Beautiful solution :)

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 standard library adopts these bounds for its traits , it won’t be able to relax them in the future. Which likely means that most of the ecosystem won’t either.

Copy link
Member

Choose a reason for hiding this comment

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

Thinking about it some more... doesn't this prevent impl<T: (const) PartialEq> PartialEq for Box<T> with a const fn inside?

Copy link
Contributor

@traviscross traviscross Mar 11, 2025

Choose a reason for hiding this comment

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

Thus we require that all traits with (const) methods also have a (const) Destruct super trait bound... We may relax this requirement in the future or make it implied.

I know this is something I had proposed, but where @nikomatsakis and I ended up landing was pushing these bounds to the use-site, which would imply not requiring this bound. This doesn't save space for making it implicit then, of course, but we could always still do that over an edition (with some kind of opt-out).

One reason for not requiring this supertrait bound is that, if we were to, it of course wouldn't be possible for traits to backward-compatibly relax it if we later removed this requirement.


## `(const)` bounds on `Drop` impls

It is legal to add `(const)` to `Drop` impls' bounds, even thought the struct doesn't have them:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
It is legal to add `(const)` to `Drop` impls' bounds, even thought the struct doesn't have them:
It is legal to add `(const)` to `Drop` impls' bounds, even though the struct doesn't have them:


## Adding any feature at all around constness

I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable code for compile time evaluation.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable code for compile time evaluation.
I think we've reached the point where all critics have agreed that this one kind of effect system is unavoidable since we want to be able to write maintainable generic code for compile time evaluation.

having to type a sigil, when there is no meaning for `const fn` without a sigil.

While I see the allure from a language nerd perspective to give every meaning its own syntax, I believe it is much more practical to
just call all of these `const` and only separate the `(const) Trait` bounds from `const Trait` bounds.
Copy link
Member

Choose a reason for hiding this comment

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

If it'd just be bounds, that'd be nice... but it's also (const) fn in traits. The argument that "we only use (const) when it has a different meaning than const" still applies, but I expect that by a wide margin, the common case will be (const) fn in traits and const fn outside traits, and that will look oddly inconsistent.

Copy link
Contributor

Choose a reason for hiding this comment

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

The common case will be (const) fn in trait declarations, but the impls will use const fn.

Copy link
Contributor

@traviscross traviscross Mar 12, 2025

Choose a reason for hiding this comment

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

The common case will be (const) fn in trait declarations, but the impls will use const fn.

Not necessarily. The model @nikomatsakis and I landed on is that the parens in (const) fn represent what is being tied to that associated effect, both in the trait def and in the impl. In our discussion and in his write-up, Niko had a particularly good example of why the distinction in the impl matters. It looks like this:

trait Tr {
    (const) fn f();
    const fn g();
    fn h();
}

struct W<T>(T);
impl<T: (const) Default> Tr for W<T> {
    const fn f() {
        T::default(); //~ ERROR?
    }
    const fn g() {}
    const fn h() {}
}

Note that f uses a generic from the impl header.

In the model, const fn is supposed to mean that the item is "always-maybe-const". That is, that its constness is dependent only on its own generics and is independent of the implicit associated effect on the trait. That's not true for f here (as the effect on the impl header generic is tied to the associated effect in the desugaring), and to see that the T::default() call is actually OK, we end up needing to consult the trait definition. So it's more clear to write this as:

struct W<T>(T);
impl<T: (const) Default> Tr for W<T> {
    (const) fn f() {
        T::default(); //~ OK
    }
    const fn g() {}
    const fn h() {}
}

Conceptually, as well, if we're not making that T::default() call, it should be possible to indicate that we're "refining" the trait (i.e. "offering more than the trait requires") by implementing this item as "always-maybe-const" (when only "maybe-maybe-const" was required). But if we use const fn f above and consequently have to look at the trait def, then it's not possible to indicate that. Maybe this doesn't matter in practice in this case, but there is a conceptual distinction.

Another angle is that, during the RPITIT discussions, one of our design axioms was trying to preserve as best as possible the ability to copy signatures from the trait def to the trait impl. In that spirit, the ability to write (const) fn in the trait impl is important.

We did debate this one both ways, but Niko and I landed, for the moment, on (const) fn being required in the trait impls in these cases for (const) fn items, and allowed in others.

Of course, writing const fn should also work, most of the time (except as above), due to this refinement perspective.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 12, 2025

Choose a reason for hiding this comment

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

Oooh, that is a great point.

However, there is an alternative. The impl block in your examples could instead be written like so:

struct W<T>(T);
impl<T: Default> Tr for W<T> {
    const fn f()
    where
        T: (const) Default,
    {
        T::default(); //~ OK
    }
    const fn g() {}
    const fn h() {}
}

Basically, any (const) in the where clauses of the impl apply as if they had been written on each of the individual methods. But you can use explicit where clauses on said methods if you need different behavior for different ones.

This design preserves an essential property of impl blocks: you don't need to look at the trait definition to understand the signature of the items they define (other than default items not overridden).

  • Another way of expressing this is that if you delete the Trait for part of a trait impl, you are left with a perfectly valid inherent impl (disregarding coherence and default items not overridden).
  • This means that implementers can immediately start refining impls with const fn without waiting on the trait definer to adopt this feature. When said definer eventually does so, the updated impl is immediately const Trait compatible.

(I believe your second design also upholds this property; an earlier version of this comment implied that it did not, but that was based on a misunderstanding.) Edit: no it doesn’t after all?

Copy link
Member

@RalfJung RalfJung Mar 12, 2025

Choose a reason for hiding this comment

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

We did debate this one both ways, but Niko and I landed, for the moment, on (const) fn being required in the trait impls in these cases for (const) fn items, and allowed in others.

That is not reflected in the RFC, or did I misunderstand?
To make sure I understand correctly, this means the following is allowed? Or does "allowed in others" just refer to trait impls?

(const) fn default<T: (const) Default>() -> T {
    T::default()
}

Copy link
Contributor

@traviscross traviscross Mar 14, 2025

Choose a reason for hiding this comment

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

And in fact, this sort of thing is necessary if we want fully general refinement. For example, you can’t express the impl below with bounds on the impl block alone:

trait Tr {
    (const) fn d() -> Self;
}

struct W<T>(T);
impl<T: Default> Tr for W<T> {
    const fn d() -> Self
    where
        T: (const) Default,
    {
        T::default()
    }
}

(Trimmed the example.)

What's your argument that this should be accepted? As mentioned, we prove that the impl method "requires no more" than the trait by seeing if we can prove its predicates from the trait method bounds and the impl header bounds.

Here, the trait method has no bounds, and the impl header has a T: Default bound. There's no way we can prove the T: (const) Default predicate in the impl item from that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Go on, what property exactly are you saying is violated, why do you think it's violated, and why do you think that property is important?

It’s right there in my comment? (Ctrl-F “An essential property of impl blocks”)

What's your argument that this should be accepted? As mentioned, we prove that the impl method "requires no more" than the trait by seeing if we can prove its predicates from the trait method bounds and the impl header bounds.

In the non-const case, we don’t require const anything, so the impl provides a perfectly valid implementation of non-const Tr for any W<T: Default + Not<Output = Self>>. When T: const Default + const Not<Output = Self> is satisfied, then we additionally provide all that is required for a const Tr impl. When T meets only one of the two const trait bounds, we get an impl of non-const Tr + some refinement extras.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see several solutions:

See https://hackmd.io/@Jules-Bertholet/BkEksib2ye#const-bounds-on-impls for a longer explanation

Copy link
Contributor

@traviscross traviscross Mar 15, 2025

Choose a reason for hiding this comment

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

Go on, what property exactly are you saying is violated, why do you think it's violated, and why do you think that property is important?

It’s right there in my comment? (Ctrl-F “An essential property of impl blocks”)

...why do you think it's violated, and why do you think that property is important?

Also, your claim isn't true of Rust today:

Playground link

Copy link
Contributor

Choose a reason for hiding this comment

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

...why do you think it's violated,

Thinking back on what I wrote before: I erroneously implied that “if you delete the Trait for part of a trait impl, you are left with a perfectly valid inherent impl” is equivalent to “you don't need to look at the trait definition to understand the signature of the items they define (other than default items not overridden)”. It’s not, these are really two separate (albeit related) properties which are both important. The current design violates the second one, because (const) fn doesn’t work in inherent impls at all. Apologies for the confusion.

Why do you think that property is important?

The first property is important when reading code; you should be able to understand how a trait impl works without having to refer to the definition. Imagine you open up an unfamiliar codebase in your IDE, and use go-to-definition on a call to a trait method; you shouldn’t have to then also visit the trait to understand how that particular method implementation works.

The second property keeps the language simple and consistent, and therefore easier to learn and understand for everyone.

Also, your claim isn't true of Rust today:

Playground link

And as you mention there, that’s considered a flaw in the language, which the refinement RFC aims to address.

These require changing the type system, making the constness of a function pointer part of the type.
This in turn implies that a `const fn()` function pointer, a `(const) fn()` function pointer and a `fn()` function pointer could have
different `TypeId`s, which is something that requires more design and consideration to clarify whether supporting downcasting with `Any`
or just supporting `TypeId` equality checks detecting constness is desirable.
Copy link
Member

@RalfJung RalfJung Mar 11, 2025

Choose a reason for hiding this comment

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

It gets a lot more complicated than that: surely we also want people to be able to newtype such a function pointer type, so all of these are now a thing:

struct S1(fn());
struct S2(const fn());
struct S3((const) fn());

From here it is actually not far to (const) Trait bounds on types (which the RFC still claims make no sense, which is IMO an incorrect claim as I have already pointed out before).

Copy link
Contributor

@traviscross traviscross Mar 12, 2025

Choose a reason for hiding this comment

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

which the RFC still claims make no sense, which is IMO an incorrect claim

Could you perhaps elaborate on this? Some weeks ago, I had also paused on this claim, and made a mental note to look into it, but haven't yet done this analysis.

Copy link
Member

@RalfJung RalfJung Mar 12, 2025

Choose a reason for hiding this comment

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

With types like struct S3((const) fn());, clearly what we call a "type" is now more like a "function from constness to types". And thus the where clauses in a type can refer to this ambient constness via (const) Trait, similar to where clauses in a const fn.

Or, put differently: I should be able to do something like

struct MyCallback<F: (const) Fn(Arg) -> Ret>(F);

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that just because there is motivation for const-parameterized user-defined types, that does not necessarily imply that we should also allow (const) Trait bounds on said types.

Copy link
Member

@RalfJung RalfJung Mar 13, 2025

Choose a reason for hiding this comment

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

Correct, and I did not say that it would imply as much. But "such bounds make conceptually no sense" is factually incorrect. If we dismiss such bounds we should do it properly.

That said, I do think we should eventually allow such bounds. The complexity of "a type's meaning depends on the surrounding constness" will inevitably become part of the language once we have (const) fn types or dyn (const) Trait types, and if we want (const) Fn to be a thing we also have constness-dependent bounds. At that point I don't see a good reason to prevent the user from also using this in where clauses for other traits.

If a single `(const)` method is declared as `const` in the impl, all `(const)` methods must be declared as such.
It is still completely fine to just declare no methods `const` and keep the existing behaviour. Thus adding `(const)`
methods to traits is not a breaking change. Only marking more methods as `(const)` if there are already some `(const)`
methods is.
Copy link
Member

@RalfJung RalfJung Mar 12, 2025

Choose a reason for hiding this comment

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

To achieve this we also have to forbid having any const fn in an impl for a trait that does not have any (const) fn, right (except if that function is const fn in the trait)? Otherwise, we could have something like

trait T { f(); g(): }

and now a user writes

impl T for MyType {
  const fn f() {}
  fn g() {}
}

and then later the trait author adds (const) to f and g -- now the user code fails to compile.

Copy link
Member

Choose a reason for hiding this comment

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

On other words, the "refinement" approach suggested by @traviscross has to be limited or else it prevents forward compatibility here.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 12, 2025

Choose a reason for hiding this comment

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

I think this is a reflection of the fact that the RFC considers that refinement a future possibility rather than part of the main proposal. This is probably because #![feature(refine)] is still unstable.

Copy link
Contributor

@traviscross traviscross Mar 13, 2025

Choose a reason for hiding this comment

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

Probably I wouldn't say this necessarily needs to be gated on #![feature(refine)], since this mechanism is an implicit associated effect, and associated types, even the implicit ones for RPITIT and AFIT, are always refining in stable Rust today (and also because we probably need to rework that refinement RFC overall).

But yes, the mechanics I describe assume both this ("associated types/effects are always refining" style of) refinement and dropping the restriction you quote requiring that "if a single (const) method is declared as const in the impl, all (const) methods must be declared as such".

}
```

If a single `(const)` method is declared as `const` in the impl, all `(const)` methods must be declared as such.
Copy link
Member

@RalfJung RalfJung Mar 12, 2025

Choose a reason for hiding this comment

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

To me this is the biggest weakness of the current proposal: this feels too magic / implicit. There is some sort of flag on a trait (so far expressed with #[const_trait]), and a flag on the impl (so far expressed with const impl), but the RFC proposes to instead infer that flag from the contents of the trait/impl block. I find this extremely hard to think about, and it also causes issues with marker traits as has already been mentioned elsewhere.

It's particularly bad for impl blocks, since it is non-local: I have to know which functions are (const) fn (as opposed to const fn) in the trait, and then match that against const fn (and maybe (const) fn?) in the impl. I'd much rather just explicitly state that I want to write an impl that satisfies const Trait bounds, than have the compiler infer that in a non-local way.

Usually, Rust prefers explicit over implicit. What is the reason for diverging from that theme here? At the very least, the RFC should list this as an alternative and discuss why it was discarded.

Copy link

@coolcatcoder coolcatcoder Mar 12, 2025

Choose a reason for hiding this comment

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

I agree that this feels weird. Altering a single method, and having it affect all others in the impl is not what I would expect. If every (const) method has to share the same const/non-const state, then it feels like it should be set in 1 location, not multiple.

(Also, sidenote, instead of making people split up traits to have multiple const/non-const states, perhaps we could have a way of defining multiple const/non-const states, and then methods can decide which const/non-const state to use?

trait Foo {
    // Shows an idea of potentially defaulting to const, should it not be specified,
    // and cannot be inferred from the methods (All methods in the impl using the default body.).
    // Although there would have to be a lint to warn against having a default
    // if it is impossible for an impl to use the default...
    maybe_const StateOne = const;
    maybe_const StateTwo;
    
    (const: StateOne) fn one();
    (const: StateOne) fn one_other();
    
    (const: StateTwo) fn two();
    // How would we handle default bodies?
    // Perhaps get them specify that they only exist when StateTwo is in a specific state?
    //(const: StateTwo) fn has_default() where StateTwo: const {}
}

impl Foo for () {
    // This idea falls apart even more here.
    // Manually setting this is pointless, as it will always be inferred from the method two.
    maybe_const StateTwo = non-const;

    const fn one() {}
    // Error: StateOne is const according to one, but non-const according to one_other.
    fn one_other {}

    fn two() {}
}

Probably a terrible idea, but I enjoy throwing ideas at the wall, and seeing how sharp their broken fragments are.)

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 12, 2025

Choose a reason for hiding this comment

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

I say something similar in #3762 (comment). However, I think it’s important that any annotation on the impl be strictly optional. This ensures that implementers can start refining methods to be const without waiting on the trait definer, and then when the trait definer eventually updates, the new impl is instantly const Trait compatible.

Copy link
Member

@RalfJung RalfJung Mar 12, 2025

Choose a reason for hiding this comment

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

I think it’s important that any annotation on the impl be strictly optional. This ensures that implementers can start refining methods to be const without waiting on the trait definer,

As explained in #3762 (comment) I don't think this is possible with the current proposal. Or put differently, what you are saying is fundamentally incompatible with:

If a single (const) method is declared as const in the impl, all (const) methods must be declared as such.

The alternative you seem to be suggesting is that:

  1. We just drop that requirement.
  2. We say that an impl qualifies for a (const) Trait bound if if happens to have a const fn for all the (const) fn of the trait.
  3. We offer some way to assert to the compiler that an impl we are writing qualifies for a (const) Trait bounds, e.g. (const) impl Trait for Type.

I would definitely prefer that over the current proposal. I don't care much for the "refinement" approach / for declaring const fn in an impl when the trait makes it such a fn (so I am not attached to number (2)), but I think the extra explicitness of number (3) is important.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 12, 2025

Choose a reason for hiding this comment

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

As I said in response to your comment, my read of that restriction is that it is meant to be temporary, to be dropped once rust-lang/rust#100706 is stable.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 14, 2025

Choose a reason for hiding this comment

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

Our impl was already tying that associated effect to the constness of the impl item.

This is the problem. You need to tie the associated effect to whether the impl item is const in all cases where the requirements for constness laid out in the trait definition are met. If the compiler doesn’t check that properly, obviously things will break. If the model can’t handle that, then it is flawed, and we should work to improve it.

Worse, the impl can't migrate to being a const impl without dropping (const) from that T: (const) A bound, which means that it can't call g any longer. So it's stuck.

This is strictly better than a fully non-const impl not being able to become fully const because it calls g. Again, not a problem at all; (): const Tr will continue to not hold, and that’s perfectly OK.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 14, 2025

Choose a reason for hiding this comment

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

IOW:

To implement Tr, an impl needs to provide a certain set of guarantees $X$. To implement const Tr, it needs to provide a strictly stronger set of guarantees $Z$. If an impl Tr for () block uses refinement to provide a set of guarantees $Y$ that is stronger that $X$ but weaker than $Z$, then () implements Tr but not const Tr. And there is nothing wrong with that.

If the compiler were to decide that () implemented const Tr even though the impl block did not provide $Z$, then that would be a bug in the compiler, and nothing more.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps it would be most helpful if you wrote out your proposal in terms of the desugaring to the formality model.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps it would be most helpful if you wrote out your proposal in terms of the desugaring to the formality model.

I’ll try to do that.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 14, 2025

Choose a reason for hiding this comment

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

@traviscross Here is a full description of my mental model for this feature: https://hackmd.io/@Jules-Bertholet/BkEksib2ye

Hopefully, it clears up some confusion.

The last section covers explicit syntax for asserting that a trait impl is const.

Comment on lines +485 to +486
* trait impls of traits with `(const)` methods
* NOT in inherent impls, the individual `const fn` need to be annotated instead
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC this does not match the model described by #3762 (comment), correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

Go on, why don't you think so?

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 13, 2025

Choose a reason for hiding this comment

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

My understanding of the model from your comment was that a T: (const) Trait bound on the impl block means that all (const) fns inside the block are const fn when and only where T: const Trait is satisfied. There is no need to refer to the trait definition to apply this rule, therefore it should work even in inherent impls, or impls of traits with no (const) fn in the definition.

But I am now thinking I misunderstood, and this is not how it works after all?

Copy link
Contributor

@traviscross traviscross Mar 13, 2025

Choose a reason for hiding this comment

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

Have a look at the desugaring in Niko's write-up that I linked. What matters here is that for the function to call a method on the T: (const) Tr from the impl header, the effect of the function ends up becoming tied to an associated effect on the trait that then ends up tied to the T: const Tr bound for that trait. That's not at issue with an inherent impl, though admittedly it's a rather subtle point.

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.