Skip to content

Make trait methods callable in const contexts #3762

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

Open
wants to merge 23 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.

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

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.

Copy link
Member

@fmease fmease Mar 30, 2025

Choose a reason for hiding this comment

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

I'd like to see #[derive]'ing being discussed in this RFC, it's notably absent from it and shouldn't be an afterthought in my opinion.

Namely, there should be a canonical/conventional mechanism for deriving a "const trait impl" from/for an ADT.
Whether such an impl would be (unconditionally) const, conditionally const, or either depending on a "flag" would be a point for discussion.

It should be quite obvious why you'd want to const-derive a trait and why #[derive] itself shouldn't derive "conditionally const trait impls" by default.


As for the concrete design, there should be a canonical/conventional way to signal to the (built-in or user-defined) derive macro that a "const" trait impl is requested.

As @petrochenkov noted in rust-lang/rust#118580 (comment), rust-lang/rust#118580 (comment) and rust-lang/rust#118580 (comment) there are three extensible candidates:

  1. Leveraging helper attributes. At call sites, that might look like

    #[derive(One, Two)]
    #[one(const), two(const)]

    I guess. At def sites, it would just look like

    #[proc_macro_derive(One, attributes(one))]
    pub fn derive_one(_item: TokenStream) -> TokenStream {}

    That design wouldn't require any additional changes to the language.

  2. Giving derive macros a second input stream. At call sites, that might look like

    #[derive(One(const), Two(const))]

    At def sites:

    #[proc_macro_derive(One)]
    pub fn derive_one(_item: TokenStream, _arg: TokenStream) -> TokenStream {}

    Note that the one-argument form would still exist for backward compatibility. This arity-based dispatch is very much possible for proc macros. See e.g., PR Provide a way for custom derives to know if they were invoked via #[derive_const] rust#118580. Of course, we could choose to do something different here but in any case I'd prefer if we didn't force a new edition for this.

  3. Passing another input stream via "global data" (which is what e.g., Span::call_site() uses under the hood). At call sites, that might look like

    #[derive(One(const), Two(const))]

    At def sites:

    #[proc_macro_derive(One)]
    pub fn derive_one(_item: TokenStream) -> TokenStream {
       let _arg/*: TokenStream */ = extra_input_stream();}

For context, the compiler currently features the placeholder syntax #[derive_const] under the experimental feature derive_const. Obviously, that syntax is quite awkward and non-extensible (to other potential effects or data in general we might want to pass to proc macros in the future).

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 the derive syntax should reflect the bound syntax: #[derive(const Trait)].

Copy link
Contributor

@traviscross traviscross Mar 31, 2025

Choose a reason for hiding this comment

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

It should be quite obvious... why #[derive] itself shouldn't derive "conditionally const trait impls" by default.

Perhaps you could elaborate on this. I can think of some reasons, but I want to be sure I've thought of all the reasons.

I think the derive syntax should reflect the bound syntax: #[derive(const Trait)].

For a related syntax discussion, see:

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Mar 31, 2025

Choose a reason for hiding this comment

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

For a related syntax discussion, see #3715

unsafe is a property of the derivation operation there, while const is a property of the resulting derived impl here. The syntaxes for each should be different, to reflect that distinction.

Copy link
Member

@fmease fmease Mar 31, 2025

Choose a reason for hiding this comment

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

It should be quite obvious... why #[derive] itself shouldn't derive "conditionally const trait impls" by default.

Perhaps you could elaborate on this. I can think of some reasons, but I want to sure I've thought of all the reasons.

Yes, of course. For the sake of argument, let's assume that this RFC establishes the convention for derive macros to generate const trait impls either always or just by default.

  1. It would create a medium-term divide in the ecosystem: The large corpus of derive macros written by users before the hypothetical stabilization of feature const_trait_impl would naturally continue to generate non-const trait impls until migrated (which takes time) contrary to the ones created after it. This could be perceived as confusing by newcomers and frustrating by seasoned Rust devs.
  2. If not made an edition item, it would implicitly and practically irrevocably alter the public API of all library crates which expose types to which built-in derive macros (from core / std) were applied as suddenly said crates would promise more (conditionally) const impls which they can't go back on without a major version update.
  3. (Conditionally) const trait impls obviously come with more requirements therefore "less can be derived (by default)". Think less about type parameters and more about the types of priv/pub fields which — from a probability perspective — likely only impl the trait in question "non-const-ly".
  4. Assuming that there's no opt-out (unlikely, I admit), going forward there would no longer be a canonical / conventional way to derive non-const trait impls (which are convenient and obviously allow for greater leeway wrt. the evolvement of the implementation) forcing users to write those impls by hand (possibly error prone) or to create their own conventions and mechanisms.
  5. Assuming that there's an opt-out, then deriving would still no longer mirror manual impls wrt. constness since constness is notoriously opt-in. That would also mean finding new syntax that's unlike anything preexisting (think #[derive(nonconst Trait)] or similar).

Feel free to correct me on / contest any of these points, some of these might be a bit weak or even irrelevant. I threw this together quickly, so yeah.

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're sticking with the already implemented #[derive_const(Trait)] syntax and leaving it to future RFCs after unsafe derives are finalized to come up with something better

Copy link
Member

@fmease fmease Mar 31, 2025

Choose a reason for hiding this comment

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

derive_const only works for built-in derive macros though, are you saying that we won't support user-defined const-derives in the foreseeable future (rust-lang/rust#118304 (blocked by design concerns or rather total lack of design))?

Comment on lines +140 to +141
* `impl const Trait` (in all positions).
* These are not part of this RFC.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this not contradict later parts of the RFC that seem to explicitly allow const in RPIT?


### Impls for conditionally const methods

Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl cosnt Trait`:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl cosnt Trait`:
Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl const Trait`:

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 `cosnt fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" 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
Contributor

Choose a reason for hiding this comment

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

Suggested change
The implementation correctness argument is partially due to our history with `cosnt fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" 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.
The implementation correctness argument is partially due to our history with `const fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" 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.

Comment on lines 227 to 229
impl<T: Default> const Default for Box<T> {
fn default() -> Self { Box::new(T::default()) }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This impl shouldn’t compile, right?

Comment on lines +329 to +346
```rust
#[const_derive(PartialEq, Eq)]
struct MyStruct<T>(T);
```

generates

```rust
impl<T: (const) PartialEq> const PartialEq for MyStruct<T> {
(const) fn eq(&self, other: &Rhs) -> bool {
self.0 == other.0
}
}

impl<T: (const) Eq> const Eq for MyStruct<T> {}
```

For this RFC, we stick with `derive_const`, because it interacts with other ongoing bits of design work (e.g., RFC 3715)
Copy link
Contributor

Choose a reason for hiding this comment

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

const_derive or derive_const?

Comment on lines +589 to +591
## Why do traits methods need to be marked as `(const)`


Copy link
Contributor

Choose a reason for hiding this comment

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

Missing body

```

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

Choose a reason for hiding this comment

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

It’s just a future possibility, so we can always figure out these details later, but I still don’t understand why this might not be possible to do. Is it just an implementation difficulty concern?

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

@traviscross traviscross Apr 1, 2025

Choose a reason for hiding this comment

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

Oli said:

The RFC requires const trait Trait and impl const Trait again after discussing it with niko.

Rationale is

  • Pro: When you change this, you have a "one shot" chance to adjust the effects of methods in the trait.

  • Pro: More clear what your intent is, allows for better diagnostics; if you add a (const) to a method without modifying the trait, we error and have a chance to explain what's happening a bit, because that may indicate you have a confused mental model.

  • Pro: Syntax mirrors usage in other places:

    • T: const Trait
    • const fn foo<T: (const) Default> and const trait Default
  • Con: Redundant. (But we could always not require it.)

Regarding the marking of the trait, I'm OK with explicit syntactic indication in the current edition. In fact, I somewhat prefer it for the reasons you mention. But I see those reasons (and in particular the number 1 and 2 "pros" that you mention) as transitional, and over time, using editions, I'd like to wash out this distinction, and that leads me to prefer using an attribute for this.

If we mark the impls with const Trait, that makes a certain sense, as it does match the bound syntax, and the const in that position means something analogous. In one place, it's setting (or asserting) the value of the associated effect, and in the other, it's bounding it. That makes sense.

On the trait, though, we'd be overloading const to mean something rather different. Probably I'd frame what it's doing as "allowing const / (const) bounds on the trait and allowing the setting or asserting of the associated effect on impls of the trait." For that one, I'd rather just have an attribute to this effect and later have an attribute that disallows those things that we'd employ when flipping the default over an edition.

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'll add it to future possibilities that we can remove the const from the trait decl and turn it into a lint or sth if you didn't add any const methods

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 there's a good semantic reason to mark the trait: it indicates the presence of an associated effect variable in the trait. non-const traits do not need an associated effect variable and thus ideally shouldn't have one to avoid mysterious issues due to a hidden degree of freedom.

Copy link
Contributor

@traviscross traviscross Apr 1, 2025

Choose a reason for hiding this comment

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

trait Tr {
    fn f();
}

impl Tr for () {
    const fn f() {}
}

fn test1<T: Tr>() {
    // `f` can't be used as `const` in a generic context.
    const { <T as Tr>::f() }; //~ ERROR
}

fn test2() {
    // Since `f` is refined as `const` for `()`, though, we can use it
    // as `const` when we know the concrete type.
    const { <() as Tr>::f() }; //~ OK
}

If we ever accept this, which I think we should, then the most sensible way to explain that, I'd suggest, is still with associated effects and the "impls of associated types/effects are always refining" rule.

That's among the set of reasons why I don't think it's prudent for us to bifurcate the space of traits long-term.

Copy link
Contributor

@traviscross traviscross Apr 1, 2025

Choose a reason for hiding this comment

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

Here's the parallel, by the way, that I draw for why that should work. This works today:

trait Tr {
    async fn f();
}

impl Tr for () {
    fn f() -> impl Future<Output = ()> + Send {
        core::future::ready(())
    }
}

fn spawn<T: Send>(_: T) {}

fn test1<T: Tr>() {
    // `f(..)` can't be used as `Send` in a generic context.
    spawn(<T as Tr>::f()); //~ ERROR
}

fn test2() {
    // Since `f(..)` is refined as `Send` for `()`, though, we can use
    // it as `Send` when we know the concrete type.
    spawn(<() as Tr>::f()); //~ OK
}

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Apr 2, 2025

Choose a reason for hiding this comment

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

Put simply, once we stabilize this, I'm planning to opt-in all of my traits and to require that in my projects for new ones.

What about traits for doing heap allocations, or I/O? For some traits, it doesn’t make sense to make them const.

Choose a reason for hiding this comment

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

What about traits for doing heap allocations, or I/O? For some traits, it doesn’t make sense to make them const.

How do you know that the implementation is performing a system call?

For example, have a look at the Store API (#3446) which would aim at replacing the Allocator API. It was specifically designed to allow "in-line" memory, ie for [T; N] to be used as the Store type and raw memory arena both, in which case even though it's an "allocation" API, no system call is required and it should be perfectly possible to call and return from a const context.

Similarly, even if the trait is FileSystem, there's no reason that a fake/mock implementation cannot be supplied in a const context.

As such, as long as a trait can be used in a const context even if some methods cannot -- for example because they return a raw file descriptor -- there's no reason not to allow a user to provide a const implementation, and allow them to use this const implementation in a const context.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, there are cases where, with extra thought and design, you might be able to design a sensible const version of the trait even in such domains. But there are also cases where you can’t (like your example of raw file descriptors, but also something as simple as “this returns a Vec”). And even where it’s possible, the trait author might choose not to do so yet, as they may not be certain of the right API, or they may want to wait for upcoming const features in the language or in their library dependencies.

Copy link
Contributor

@traviscross traviscross Apr 3, 2025

Choose a reason for hiding this comment

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

But it seems easier to just not allow refinement and avoid all this complexity.

People made this argument about refinement with RPITIT as well. We defended it on the basis that "RPITIT is just hidden associated types, and associated types are always refining." I don't believe that RPITIT would have been better without this kind of refinement. And having defended it with RPITIT, I'm probably especially skeptical that we should foreclose or plan to foreclose doing the same thing with hidden associated effects when there's a clear parallel.

Yes, until and unless we have more features there will be some limitations. We often choose to live with those for awhile while keeping doors open.

To the point of your example about blanket impls over wrapper types, there's no current way to express this either, even after we ship RTN:

trait Tr {
    async fn f(&self);
}

impl<T: Tr<f(..): (Copy)>> Tr for MyBox<T> {
    fn f(&self) -> impl Future<Output = ()> + (Copy) {
        (**self).f()
    }
}

And of course, until we ship RTN, even the version without conditional bounds isn't and hasn't been possible to express (without desugaring the trait away from AFIT/RPITIT, at which point you have to start writing manual Future impls to implement the trait since we haven't shipped ATPIT either).

Also wrt the default being const. The same could be said for const fn in general. So maybe these should be considered together.

These are not comparable. Flipping the default on fn means making all function bodies const contexts by default and then making runtime an explicit effect somehow. Flipping the default on trait doesn't mean anything like that, especially now that we're marking all items. It doesn't require us to (un-)invert an effect and all that entails.

One of my concerns about extending const to this position is that it overloads const in a different and unrelated way. The fact that even we're tempted to equivocate between these very different semantics in our own discussions just because of the shared keyword does not lessen my concern about this.

I think all this is out of scope of this RFC though

If we're going to require syntax to opt-in traits, we should settle that syntax in this RFC.

Copy link
Member

Choose a reason for hiding this comment

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

If we ever accept this, which I think we should, then the most sensible way to explain that, I'd suggest, is still with associated effects and the "impls of associated types/effects are always refining" rule.

I don't think so. The trait has no associated effect in this case. We just change how we typecheck trait method calls if we can already resolve to the impl, basically entirely bypassing the trait and treating this as if it was an inherent method call. I think we have to do that anyway, otherwise we can't explain e.g. unsafe refinement or weaker trait bounds.

So, accepting that code makes no statement at all about the trait, since the trait is being bypassed in the relevant call. We can still meaningfully say that this trait simply does not have an associated effect. And my feeling is still that since an associated effect is a new degree of freedom with tangible consequences, we need some syntax to indicate its presence.

If I try to phrase your position in this framework, I think you'd say that the associated effect doesn't actually have any consequences, we cannot observe it doing anything unless there's a const impl (i.e., unless there's an impl that sets the associated effect to anything non-default) and there you already agreed to having syntax? Hm... I'll have to think more about this.

Comment on lines 22 to 24
impl const Default for () {
const fn default() {}
}
Copy link
Contributor

@traviscross traviscross Apr 1, 2025

Choose a reason for hiding this comment

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

Oli said:

The RFC requires const trait Trait and impl const Trait again after discussing it with niko.

Rationale is

  • Pro: When you change this, you have a "one shot" chance to adjust the effects of methods in the trait.

  • Pro: More clear what your intent is, allows for better diagnostics; if you add a (const) to a method without modifying the trait, we error and have a chance to explain what's happening a bit, because that may indicate you have a confused mental model.

  • Pro: Syntax mirrors usage in other places:

    • T: const Trait
    • const fn foo<T: (const) Default> and const trait Default
  • Con: Redundant. (But we could always not require it.)

For the marking of the impl, I'm OK with impl const Trait (if we decide we want to mark impls), as it matches the bound syntax and means an analogous thing. In one place, it's setting (or asserting) the value of the associated effect, and in the other, it's bounding it. That makes sense.

Unlike with marking the trait, I don't see this one as transitional.

On the question of whether we want to mark the impls, I'll say that not doing so feels approximately equivalent to me to how one has to consider all of the fields of a type to know what auto traits that type implements, and marking it feels similar to #[derive(Copy)], for all of the good and bad that each of those entail.

Another element to consider is that if we don't mark the impl, then conceivably we could allow people to constify their impls ahead of the trait being updated. Then, those impls would automatically qualify for the bound when the trait was marked for that. I don't know how important that is. I could imagine maybe people caring who want their thing to work with both newer and older versions of some upstream crate for a trait and want their own downstreams to be able to take advantage of the const bound when using the new upstream crate version. Marking the impl means this ecosystem transition has to be serialized. Maybe or maybe not that matters, but it's something to think about.

If we were to see impl const Trait as essentially just a static assertion on the associated effect, maybe that opens another door of syntactic possibilities for this.

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Apr 1, 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 see impl const Trait as essentially just a static assertion on the associated effect

I’ve argued in favor of this previously, but it doesn’t strictly need to be in the MVP. However, if we want to be forward-compatible with this, we need a special case now for const Traits with no non-defaulted (const) fns—they should probably be forbidden for now. (We don’t want people relying on certain types only implementing non-const Trait, and then a rustc upgrade makes them implement const Trait also)

* super trait bounds
* where bounds
* associated type bounds
* return position impl trait
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Apr 1, 2025

Choose a reason for hiding this comment

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

Can you give an example of RPIT/describe the semantics?

@Yokinman

This comment was marked as duplicate.

@oli-obk
Copy link
Contributor Author

oli-obk commented Apr 9, 2025

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

Comment on lines +28 to +30
impl<T: (const) Default> const Default for Thing<T> {
(const) fn default() -> Self { Self(T::default()) }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Over in rust-lang/rust#139858 (comment), there was an ask to start a thread here about this (const) fn syntax on methods in the impl.

Requiring this (const) fn syntax in cases like the one above, where the method's constness as implemented necessarily depends on the constness of a generic from the impl header -- and consequently on the associated effect in the desugaring -- is where @nikomatsakis and I had landed back at the start of March. We had discussed it both ways, but we liked this one better in our our discussion. Among other things, we liked how this emphasizes the connection to the associated effect, and this syntax allows for copying signatures from the trait def to the impl.

Niko mentioned this in his write-up here, and I elaborated on this earlier in this thread in #3762 (comment) and in #3762 (comment). Please see those comments, particularly the first, for more details.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This I'd a copy paste error. I have never intentionally written this syntax

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just more syntax salt solely for the purpose of some language purity. There is no meaningful difference unless we stop marking impls which would be a mess

Copy link
Member

@RalfJung RalfJung Apr 16, 2025

Choose a reason for hiding this comment

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

This is just more syntax salt solely for the purpose of some language purity.

No, it is using syntax to convey a conceptual difference: is this function's constness tied to the constness of the trait (and therefore dependent on (const) bounds in the impl header) or not?

At least, that's what I understood it to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, but is this difference actually useful? We need to check whether const fn refers to a (const) fn in the trait or a const fn in the trait anyway. This is just a random thing users will need to learn without actually caring. Only from a language purity this is nice, but practically this has neither an effect on the compiler (beyond being annoying to implement) nor on users' ability to understand the code

Copy link
Member

Choose a reason for hiding this comment

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

It seems quite relevant to me to understand which bounds I have in context. A const fn cannot use (const) Trait bounds of the impl block, right?

Copy link
Contributor Author

@oli-obk oli-obk Apr 16, 2025

Choose a reason for hiding this comment

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

if it was declared as (const) fn in the trait it can afaict. Or I still don't understand the TC/niko model so maybe someone else should write this RFC because I'm lost honestly and it seems bad to have someone who dislikes a model (the one allowing const, (const) and normal fns in one trait) being the one to write the RFC just to walk into mines like these.

see also my confusion in #3762 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

the one allowing const, (const) and normal fns in one trait

Yeah fair, on that one I am with you, I don't think we need that for the MVP -- though we should keep it in mind as a future possibility.

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.