Skip to content

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

Related:

@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

@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

Choose a reason for hiding this comment

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

As someone who has spent much time helping people getting their hands on Rust, I'd like to second this.

I feel like having a Destruct trait which is used only in this case but is different than Drop (which you should not use in bounds anyway) is really confusing. This is not something that you can understand without knowing about rustc internals or the language history goes backward to the language great consistency.

Most people just want to know "can I drop this at compile time or not", they don't care if it is because of a manual Drop impl or drop glue. Drop is already a special trait, I think we can make it mean something else for ~const bounds (and maybe change it later for normal bounds).

In short, I think that adding ~const Destruct instead of ~const Drop would make the language harder to understand.

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.

Most of the time you don't want to write out your impls by hand, but instead derive them as the implementation is obvious from your data structure.

```rust
#[const_derive(PartialEq, Eq)]
Copy link

Choose a reason for hiding this comment

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

This looks like a typo - you talk below (consistently) about derive_const, but this is const_derive.

Copy link

@clarfonthey clarfonthey left a comment

Choose a reason for hiding this comment

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

Going to share this comment I made here: rust-lang/rust#90080 (comment)

Specifically, I'd like to point out that rust-lang/rust#67792 is a currently locked issue still being added as a tracking issue for several things depending on this RFC, with the caveat that "a new issue will be opened when the RFC is accepted."

We all know that this RFC is going to be accepted in some form or another, just, not sure what its final form will be. There are currently plenty of discussions happening in the background behind closed doors that have been influencing implementations (for example, rust-lang/rust#139858 mentions that the lang team decided on a new syntax but there is no link to where that discussion happened), and currently, they are all stuck on this old, locked tracking issue where nobody else is allowed to participate except those directly under the rust-lang org.

This should be resolved properly. Either, there needs to be a consensus that new stuff should not be done with the const traits (syntax, making traits const, etc.), or it should be acknowledged that things are being done under the assumption this RFC will be accepted in some form, and either the old tracking issues should be unlocked, or new ones should be made.

EDIT: I tried to follow your "only threadable discussions", "reminder" (it's not a reminder, it's a request, since this is not how the GitHub UI is designed to be used), but apparently leaving a review without linking it directly to a line in the source doesn't accomplish this. Sorry; feel free to create a new "thread," or just resolve this in a single comment so there's no need to have a thread.

@traviscross
Copy link
Contributor

traviscross commented Jul 9, 2025

There are currently plenty of discussions happening in the background behind closed doors that have been influencing implementations (for example, rust-lang/rust#139858 mentions that the lang team decided on a new syntax but there is no link to where that discussion happened)

When @oli-obk said, over in rust-lang/rust#139858, that,

Afaict the syntax with which to move forward was settled as much as it could be in the last lang team design meeting. The lang team wants to get hands-on experience with the feature to see how it feels, so this PR is ready to be merged now.

he was referring to:

In that meeting, we didn't decide on the syntax. But, as Oli said, we need hands-on experience here, and along with the new proposal presented in that meeting and the lack of immediate strong objections, that's enough to move forward the experimental implementation work, as Oli did.

The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there.

@RalfJung
Copy link
Member

RalfJung commented Jul 9, 2025

Nevertheless, having proper tracking issues would be helpful. The current situation of reusing either 5-year old locked issues or 5-year-old closed issues as tracking issues is very confusing.

@traviscross
Copy link
Contributor

traviscross commented Jul 9, 2025

Agree of course we should properly track things. In my view, rust-lang/rust#67792 is still the proper tracking issue for this work (as with many of our tracking issues, of course, it could use a lot of love to be brought up to date). I've gone ahead and unlocked it. I'd expect the reason we needed to lock it was temporary and hopefully the need for that has passed. If that's not the case, we can always lock it again.

@clarfonthey
Copy link

The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there.

I should clarify, I wasn't saying that the information wasn't technically available, just that it wasn't immediately obvious where to look. For example, even among lang team meetings, it's not immediately clear which one it took place in, and the more time that passes between when a decision was made and when the decision is noticed by someone else, the harder it is to find which specific discussion occurred if it's not linked. It's entirely plausible that the discussion happened multiple meetings before the PR and it was only at that point where someone got around to implementing it.

This is kind of the reason why tracking issues exist: instead of linking every micro-discussion that happened elsewhere when it comes to important changes with respect to a thing, the unified tracking issue tracks any relevant decisions that happened. Precisely so you don't have to look through a bunch of irrelevant discussions in meeting notes, you can just have a bullet point in the tracking issue pointing out a PR that was made, which mentions in its description that it was after a discussion in a lang team meeting.

(Thank you for unlocking the issue, by the way; I'm mostly just adding a bit more context since I'm not 100% sure my point was clear.)


### Const trait bounds

Any item that can have trait bounds can also have `const Trait` bounds.
Copy link

@chrisbouchard chrisbouchard Jul 13, 2025

Choose a reason for hiding this comment

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

Is it allowed to write const Trait or [const] Trait bounds when Trait is not declared #[const_trait]/[const]?

For example:

trait Foo {}

const fn bar<F>()
where
    F: [const] Foo,  // <-- Is this an error?
{}

I ask because currently the const_trait_impl feature on nightly does require #[const_trait] in order to write const and [const] bounds. So currently the example would fail to compile on nightly with something like:

error: `[const] can only be applied to `#[const_trait]` traits

I wasn't clear whether this is the intended behavior, or just an unstable feature being unstable. The quoted sentence here makes it sound to me like the bound should be allowed. But maybe it's ambiguous—it could also be interpreted as only defining where the const/[const] bound syntax is permitted, without precluding further restrictions. (Sorry if I missed a more relevant part of the RFC.)

I understand that const Trait impls require Trait to be declared [const], which helps ensure forward compatibility. But I don't think that concern would carry over to const Trait and [const] Trait bounds—they would just be trivially unsatisfiable if a const Trait impl couldn't exist.

I think allowing this would be very useful to library authors, who could write [const] Trait bounds on Trait from external crates, while still supporting older versions of the crate where Trait isn't declared [const] yet. Adding a new const fn in crate A isn't a breaking change; nor is adding [const]/#[const_trait] to Trait in crate B (I think?). But if A had to bump the MSV of its dependency on B to add a new const fn with a [const] Trait bound, that would be a breaking change for crate A, which would make it more painful to start using the new feature gradually.

Copy link
Contributor

Choose a reason for hiding this comment

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

The sentence applies to the item the trait bound is applied to, not the trait definition. The currently implemented behavior matches the RFC’s intent.

Choose a reason for hiding this comment

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

It would also simplify life for macro writers, in a similar way to allow_trivial_constraints.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

trait Foo {
    fn foo<T: Bar>(); 
}

Cannot be implemented without the trait being marked, as otherwise you are making a decision for the trait author as to whether that bound is not const or conditionally const

There's lots of semver issues around that, I'm gonna add a section, but TLDR is "not possible for traits with generic methods, associated type bounds or super traits other than Sized"

Copy link
Member

Choose a reason for hiding this comment

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

That's not what they were tasking about though? They were asking about bounds, not impls:

trait Foo { ... }

// Forbidden
impl const Foo for ... { ... }

// Could be allowed?
const fn bar<T: [const] Foo>(x: T) { ... }

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jul 14, 2025

Choose a reason for hiding this comment

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

@chrisbouchard That’s a good example, thanks! I guess I have to amend my statement: [const] should be forbidden in the where clauses of these items. In your example, the [const] Input bound is not in a where clause, so it doesn’t break the property I listed earlier, and should be allowed. (I had never really thought about the fact that type Assoc: Supertrait; and type Assoc where Self::Assoc: Supertrait; mean different things. Would make a nice question for https://github.com/dtolnay/rust-quiz …)

Choose a reason for hiding this comment

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

@Jules-Bertholet Hmmm. Again, I feel like [const] bounds in associated items' where clauses could be useful, but I haven't fully thought it through yet.

Could you express something like

[const] trait Trait<T> {
    type Collection: [const] IntoIterator<Item=T>
    where
        <Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>;

without a [const] bound in the where clause? (In a future where the iterator traits are [const].)

At least in this case, I think you could replace it with a second associated type

[const] trait Trait<T> {
    type Iter: [const] Iterator<Item=T>;
    type Collection: [const] IntoIterator<Item=T, IntoIter=Self::Iter>;

at the expense of a second associated type, which forces impls to spell out the iterator type.

(I suppose in this const iterator future IntoIterator::IntoIter may already be [const] Iterator, but not necessarily. E.g., you could construct an iterator at compile time that can only be iterated at runtime.)

I do feel like I see the outline of your concern, but I don't think I understand it fully. Could you share an example where a [const] bound in an item where clause makes const Trait surprisingly weaker than Trait? Thanks!

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jul 14, 2025

Choose a reason for hiding this comment

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

Could you express something like

[const] trait Trait<T> {
    type Collection: [const] IntoIterator<Item=T>
    where
        <Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>;
}

without a [const] bound in the where clause?

Yes. Just move the where clause up:

[const] trait Trait<T>
where
    <Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>,
{
    type Collection: [const] IntoIterator<Item=T>;
}

Copy link
Member

Choose a reason for hiding this comment

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

Could you express something like

[const] trait Trait<T> {
    type Collection: [const] IntoIterator<Item=T>
    where
        <Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>;

without a [const] bound in the where clause?

or you could use the following, but it's not as general:

[const] trait Trait<T> {
    type Collection: [const] IntoIterator<Item = T, IntoIter: [const] Iterator<Item = T>>;

Copy link
Contributor

Choose a reason for hiding this comment

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

The place to look for what would become inexpressible here are associated functions (including e.g. RPITIT ones) with these [const] WC bounds.


### Conditionally const traits methods

Traits need to opt-in to allowing their impls to have const methods. Thus you need to mark the trait as `[const]` and all the methods will become const callable.

Choose a reason for hiding this comment

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

It took me a while to understand why this opt-in is needed, so here is a rewording suggestion that will make it more obvious:

Suggested change
Traits need to opt-in to allowing their impls to have const methods. Thus you need to mark the trait as `[const]` and all the methods will become const callable.
Making a trait's methods callable in const contexts imposes extra constraints on their default implementations (if any).
Therefore, traits need to opt-in to allowing their impls to have const methods.
You need to mark the trait as `[const]` and all the methods will become const callable.

@FeldrinH
Copy link

FeldrinH commented Jul 27, 2025

I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in [const] somewhat awkward. In other parts of Rust square brackets already mean the creation or indexing of some container, so using them here to indicate another thing alltogether feels like its overloading the meaning of square brackets too much.

I much preferred the old ~const syntax. It also had a pleasing symmetry with the other prefix modifiers like ?Trait and !Trait.

@npmccallum
Copy link
Contributor

I was playing around with this today. And I noticed a potential inconsistency. In trait definitions, you have this theoretical syntax:

const unsafe trait Foo { ... }

But in implementation, you have this theoretical syntax:

unsafe impl const Foo ...

Perhaps it should be this?

const unsafe impl Foo ...

I suspect const is a property of the implementation not of the trait -- precisely the same as unsafe.

@teohhanhui
Copy link

@npmccallum There is the precedent of unsafe const fn. Wouldn't it be more consistent to go with unsafe const impl then?

But then there's still the issue that the RFC proposes impl const Trait, so only unsafe impl const Trait would be consistent with that...

@npmccallum
Copy link
Contributor

npmccallum commented Aug 9, 2025

@npmccallum There is the precedent of unsafe const fn. Wouldn't it be more consistent to go with unsafe const impl then?

But then there's still the issue that the RFC proposes impl const Trait, so only unsafe impl const Trait would be consistent with that...

$ rustc --crate-type lib - <<EOF
const unsafe fn const_first() {}
unsafe const fn unsafe_first() {}
EOF
error: expected one of `extern` or `fn`, found keyword `const`
 --> <anon>:2:8
  |
2 | unsafe const fn unsafe_first() {}
  | -------^^^^^
  | |      |
  | |      expected one of `extern` or `fn`
  | help: `const` must come before `unsafe`: `const unsafe`
  |
  = note: keyword order for functions declaration is `pub`, `default`, `const`, `async`, `unsafe`, `extern`

error: aborting due to 1 previous error

@npmccallum
Copy link
Contributor

Also, rust-lang/rust#143879 puts const before unsafe.

To be clear, I don't care about the ordering of const vs unsafe. I'm only pointing out that unsafe trait requires unsafe impl but const trait requires impl const. That seems inconsistent to me.

@teohhanhui
Copy link

In light of that, from what I can tell the syntax needs to be either:

  1. const impl Trait / const unsafe impl Trait
  2. impl const Trait / impl const unsafe Trait
  3. impl const Trait / unsafe impl const Trait

to be self-consistent.

2 is impossible because the existing syntax is unsafe impl.

@Jules-Bertholet
Copy link
Contributor

I suspect const is a property of the implementation not of the trait -- precisely the same as unsafe.

const is part of the identification of the interface being implemented: “this impl block implements the const version of the trait”. unsafe is a modifier for the implementation itself: “I assert that this impl block fulfills the safety preconditions for implementing the trait”. It makes perfect sense, from that perspective, for unsafe to precede impl but const to precede the trait.

@clarfonthey
Copy link

Regardless, I think it's worth mentioning that there should be parser recovery and proper linting to fix the keyword order here if people mess it up, like we have for ordinary functions.

@FeldrinH
Copy link

FeldrinH commented Aug 19, 2025

#3762 (comment):

I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in [const] somewhat awkward. In other parts of Rust square brackets already mean the creation or indexing of some container, so using them here to indicate another thing alltogether feels like its overloading the meaning of square brackets too much.

I much preferred the old ~const syntax. It also had a pleasing symmetry with the other prefix modifiers like ?Trait and !Trait.

@oli-obk Any comment on this? Is the [const] syntax final?

@oli-obk
Copy link
Contributor Author

oli-obk commented Aug 19, 2025

No, but it is our best candidate at this time. We've had this bikeshed in 50 directions, it is not useful to restart it on this RFC. Please start a thread on the #T-lang/effects stream. But be aware that syntaxes have been discussed to death and there isn't much energy in T-lang or us compiler folk working on the feature for those discussions. While it sucks because of the distributed nature of all the previous discussions, I would really prefer if you read those first

@FeldrinH
Copy link

FeldrinH commented Aug 20, 2025

No, but it is our best candidate at this time. We've had this bikeshed in 50 directions, it is not useful to restart it on this RFC. Please start a thread on the #T-lang/effects stream. But be aware that syntaxes have been discussed to death and there isn't much energy in T-lang or us compiler folk working on the feature for those discussions. While it sucks because of the distributed nature of all the previous discussions, I would really prefer if you read those first

I took a look at #T-lang/effects and could not find those discussions. I am sure they are somewhere in there, but the discoverability of stuff on Zulip is unfortunately not great.

It would be really nice if the RFC could include some overivew of these discussions and the current consensus of people working on this.

as they need to actually call the generic `FnOnce` argument or nested `PartialEq` impls.


# Future possibilities

Choose a reason for hiding this comment

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

Since this RFC proposes the Destruct trait, it might be appropriate to mention allowing ?Destruct without const1 as a future possibility.

Footnotes

  1. for example, opting out of Destruct in favor of a future async Destruct, or having a type require some other data to be dropped

@clarfonthey
Copy link

I believe that the [const] syntax was specifically decided in a lang team meeting, and as far as I'm aware, it's been substantially less discussed than the former ~const syntax. That doesn't mean it's necessarily better or worse, but I do think that a lot of people, myself included, have absolutely no idea what the justification was for it beyond "lang team wanted to try it out."

I get that these discussions are exhausting, and I don't think that syntax should necessarily block this RFC, but I do think that it's worthwhile collecting all those discussions in one place, rather than just asking people to scour Zulip for them. That is the purpose of an RFC, after all: explaining everything in one place so people don't have to piece everything together themselves from several fragmented discussion threads.

@Jules-Bertholet
Copy link
Contributor

Nothing has been decided. My understanding is that there are some people on the lang team who like [const], but there is no consensus, and it hasn’t been discussed much in lang team meetings. The current nightly impl supports both, so people can experiment with whichever syntax they prefer.

I’m sympathetic to the concern of wanting discussions out in the open, but I don't think it makes sense to burden the GitHub discussion with endless conversation threads at this stage, while things remain so open-ended.

@clarfonthey
Copy link

I should be clear: I don't think that we should copy all of the discussions that were had here or re-discuss them here, but I do think it would be nice if, at some point, we could collect them in a useful/actionable summary that could be added to the RFC. Honestly, it could even be added to the RFC after the fact, since like I said, I don't think this is something we need to block the RFC on when the semantics are the important part to nail down here. Even if we end up bickering later about the syntax, that should just be about the syntax, and not the semantics, which we should set in stone here.

Plus, GitHub constantly makes it difficult to have these discussions in the first place anyway, since it loves hiding every other comment on an issue thread. Overwhelmed by comments? Let's encourage more of them because people don't notice that something hasn't been discussed, because it was hidden, because we figured that you'd otherwise be overwhelmed.

Keeping things in their own review threads is an okay workaround, but it still doesn't do great for more all-encompassing discussions like these that don't really have a good spot in the diff to point to. That said, I'll make this my last comment on the matter for now. My main point was that I think it's vitally important to summarise these discussions as they happen because, well, it ends up frustrating for everyone involved if that doesn't happen: those who had the discussions have no energy left to summarise them because there were so many of them, and those who didn't partake in the discussions have no idea where to find them.

@npmccallum
Copy link
Contributor

FYI, I decided to throw together a crate to help increase testing of this feature:

https://crates.io/crates/c0nst

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.

# Alternatives
Copy link
Contributor

@matklad matklad Aug 21, 2025

Choose a reason for hiding this comment

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

I think two important alternatives are missing from the alternatives section. While it is clear that
the team is set on the general direction, I think it's worth-while to at least mention them:

Alternative I, automatic const

We allow running any code at compile time, as long as it doesn't invoke
FFI/syscalls/target-specifific intrinsics, etc. Errors are moved from const fn declarations to
calls in const context.

Alternative II, optimistic const

Aka, the C++ approach. C++ also has const (called constexpr), but, as far am aware, they don't
have a second [const] modality. I think this works becaues constexpr means that a function
"may" be const-evalable for some values of the arguments, not that it "must" be const-evalable for
all arrguments. Here's the litmus test example of the semantics:

#include <iostream>

constexpr int f(bool b) {
    if (b) std::cout << "See you at runtime!";
    return 92;
}

int g(bool b) {
    if (b) std::cout << "See you at runtime!";
    return 92;
}

constexpr int a = f(true);  // error
constexpr int b = f(false); // works (!!!)
constexpr int c = g(false); // error

I am a bit surpsised that these alternatives don't get at least some considiration. They of course
push some of the semver checking from compiler to the human, but the benefit of semver checking
constness seems much less important than for trait checking, and doesn't allow separate
type-checking (you still have to actually run consteval in the end of the day, and it still can
fail with division by zero or some such). In particular, C++ version does sound like it might be a sweet spot? It allows library authors to declare their intention, and to (conservatively) make functions as definitely-non-const, while avoiding the entire mechanism of const-conditionality.

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 the reason Rust has not considered or rejected those alternatives is because it goes against the precedent we have in the trait system compared to C++'s template system where in Rust everything is checked ahead of time rather than leaving it for later like C++ where you have no idea if it works until you try and use it whereas Rust gives you an error immediately making it much more resilient against bugs.

Copy link
Contributor

Choose a reason for hiding this comment

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

where in Rust everything is checked ahead of time rather than leaving it for later

This is not true for const generics/const fn.

fn foo<const N: u32>() -> u32 {
    const { N - 1 }
}

fn main() {
    foo::<1>(); 
    foo::<0>(); // Doesn't compile.
}

More generally, given const X: u32 = some_function(), you can’t really say whether this’ll work or not, you have to evaluate the function to check whether it terminates. const annotations doesn’t really change anything about this.

It does seem to me that theres a very meaningful difference here in how traits and const fn s behave.

Copy link
Member

Choose a reason for hiding this comment

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

CC [@]fee1-dead's very good reply to [@]matklad's general proposal which was first posted on lobste.rs: https://lobste.rs/s/jslvmu/const_trait_counterexamples#c_ixouyi

Copy link
Member

Choose a reason for hiding this comment

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

I don't think I fully get the semver point raised there. Is the point that adding std::cout << "See you at runtime!" breaks semver? That's just like adding panic! to a const fn though. Any change in behavior in a const fn could break compilation of a downstream crate. I could see that it is easier to realize that panic! can break users than calling a function without realizing it is not const fn, but that seems like a quantitative difference to me, not a qualitative one. So I do agree that it is worth weighing this against the complexity we have to pile up to avoid such accidental non-const-fn calls.

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 we should try to avoid what happens in Python where you pass in some object and you only figure out that the object didn't support what you need when you run it and hit some corner case, or even worse, your library needs a particular interface but doesn't use every part of it, but a later version does use the rest of it, but now that breaks all your users who were just passing in objects that implemented only whatever the previous version of your library used.

Copy link
Contributor

@matklad matklad Aug 22, 2025

Choose a reason for hiding this comment

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

Indeed, though we can preserve some checking, and there's actually some flexibility in what we allow. I think the C++ semantics is this:

fn g() {}

constexpr fn f1() { g() } // declaration site error
constexpr fn f2(b: bool) {
    if b { g() } // errors when f2(true) is called
}

that is, you get early errors if you are clearly calling something non-constexpr.

We can actually go further and make both of those into errors, and leave dynamic errors only for calls that can't be resolved statically (when you actually have T: Eq type parameter, and not when you use concrete Foo::eq).

Copy link
Member

Choose a reason for hiding this comment

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

well, I would like it to also error when you have:

pub const fn f<T: PartialEq>(a: &T, b: &T) {
    a == b
}

struct S;

impl PartialEq for S {
    // not a const fn
    fn eq(&self, other: &S) -> bool {
        if std::env::var("LOG").is_some() {
            eprintln!("S::eq");
        }
        true
    }
}

// errors even though nothing calls g
pub const fn g() {
    f(&S, &S);
}

Choose a reason for hiding this comment

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

As a C++ person that was sent a link to this discussion, having to put constexpr on everything is annoying. And imagining a definition checked C++ where you not only have to mark the function as constexpr but also the constraints is even more annoying.

C++ has reached a point where essentially everything is constexpr as long as it is defined in a header, doesn't access global state, and doesn't call C functions. You can (and should, if you're a library author) write a lot of C++ code that is constexpr. As someone who primarily writes library code at work, I don't remember the last time I have written a non-constexpr non-template function.

If Rust compile-time execution follows the C++ trajectory, even more Rust code can be const than C++ code constexpr: Global variable access is unsafe and thus discouraged, and you don't have header/source files, so that limitation doesn't exist either. I'd bet in 5-10 years, Rust library developers are as annoyed with mandatory const annotations as C++ library developers are right now with the constexpr annotations.

I'm not involved in Rust, but I'd recommend that you explore the Alternative Ⅱ or even Ⅰ. Yes, the accidental semver violations are scary, but that problems also exist for something else—access to OS APIs. I made this argument five years ago in the C++ world (https://www.foonathan.net/2020/10/constexpr-platform/), but I'd argue that long-term const should be treated just like e.g. Windows-specific functions should be treated, as a platform a crate can or cannot target. You don't have a windows annotation on functions that can be executed on Windows, and executing a function that isn't marked windows on Windows is impossible, because what if a future version wants to use Linux specific APIs? A crate just documents that it works on Windows.

Long term, once almost all Rust code can be executed at compile-time, you want to treat it just like any other platform you can execute Rust code on. Having to put mandatory annotations on everything at that point just means that the keyword to introduce a function is no longer fn but const fn.

Anyways, just my unsolicited 2cts.

Copy link

Choose a reason for hiding this comment

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

@foonathan

https://www.foonathan.net/2020/10/constexpr-platform/

Good article, the idea is interesting and does deserves a mention in "Alternatives". But there may be cultural difference that Rust prefers doing precise constraint validation via type system instead of in human language (documentations) if possible. And I personally like the former because human language is imprecise, unverifiable, and far more complex than all RFC combined1.

You don't have a windows annotation on functions that can be executed on Windows, and executing a function that isn't marked windows on Windows is impossible, because what if a future version wants to use Linux specific APIs?

That's not exactly true in Rust. We do have cfg(windows) module which exposes Windows-only APIs. They are not callable on non-Windows platforms and are guaranteed (by semver) never call a Linux-only-API in the future. Generic APIs are exposed in generic modules with looser constraints: users can expected them to be callable, working and NOT instantly panic where they can call them (returning Err(_) is still a valid and working impl). In the consteval world, this "expectation" translates to "if there is a pub const fn with some constraints, you can expect calling it under specified constraints should always work".

Yes, someone can still violate this expectation by panic!() inside, but that serves an escape hatch for unwritable complex constraints, which we are constantly reducing. Otherwise, the library author is to blame, not the caller, similar to an non-unsafe fn causes UB when it's called weirdly in a safe context (is considered the library's fault).

@matklad

Indeed, though we can preserve some checking, and there's actually some flexibility in what we allow. I think the C++ semantics is this:

fn g() {}

constexpr fn f1() { g() } // declaration site error
constexpr fn f2(b: bool) {
    if b { g() } // errors when f2(true) is called
}

If we accept this and simply propagate (semantically, not type-wise) all implicit preconditions to the caller const fn instead of enforcing them, just after some layers of calls, preconditions grow unmanageable2: no one, including the author, knows or is able to describe what preconditions does the outermost const fn have. This is a nightmare for the caller3. Defining "calling me with inputs that makes me error is an error" is not helpful to anyone but a catchall disclaimer.
Yes it is possible today to do this via panic!, but again, I still want to blame library author, and don't want to be blamed for "calling it wrong by simply following its signature". 😢

Footnotes

  1. Because all RFCs are representable in human language.

  2. Finding a pre-image of a hash function is difficult if not impossible.

  3. Actually, post-monomorphization-error everywhere is one of the main reason why I left C++.

@jplatte
Copy link
Contributor

jplatte commented Aug 23, 2025

I guess it would have to be A: [const] Add<Error: [const] Destruct>? Which is really kind of awkward.

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.