Stabilizing a const generics MVP

@lcnr mentioned "no where bounds," but I'm not sure exactly what they meant by this.

My initial goal is to try and have the const generics MVP not depend on lazy_normalization_consts.

This causes cycle errors when const expressions are used in bounds:


pub trait Foo<const B: bool> {}
pub fn bar<T: Foo<{ true }>>() {}
//                ^^^^^^^^ cycle error

These cycle errors are really easy to hit and have horrible error messages, so I don't want them on stable.

The two solutions are probably to either to wait until lazy_normalization_consts is good enough to also get stabilized at the same time or forbid generics inside of consts inside of bounds.

We already probably have to restrict generics in this way in some other places, for example inside of type defaults. See the relevant zulip discussion for more details.

edit: we might actually have to first stabilize lazy norm here. I forgot this hack exists, will look a bit deeper into it this week. So restricting bounds this way might be unnecessary

second edit: it seems like we don't have to stabilize lazy norm first, see Stabilizing a const generics MVP for the relevant update


I think that an MVP with just integer constants gives us the largest effect (arrays!) while also being the safest thing to stabilize. We should do this in parallel to the already linked integer tree representation MCP Integer trees will make it safe to use aggregates and I don't think we gain much by blocking min_const_generics on this.

Note that for const generics, unlike match patterns, we must require that Eq and PartialEq are derived recursively! All variants and fields of the type must have structural equality, not just the root type that you use for your constant.


Is that not the case for match patterns as well? For example, this fails to compile:

use std::cmp::{PartialEq, Eq};

struct Inner;

impl PartialEq for Inner {
    fn eq(&self, _: &Self) -> bool { false }

impl Eq for Inner {}

#[derive(PartialEq, Eq)]
struct Outer(Inner);

const OUTER: Outer = Outer(Inner);

fn f(o: Outer) {
    match o {
        OUTER => {}
error: to use a constant of type `Inner` in a pattern, `Inner` must be annotated with `#[derive(PartialEq, Eq)]`
  --> src/
18 |         OUTER => {}
   |         ^^^^^

I wager that for the arrays use-case we hardly even need trait T<const N: $int>...

Maybe it would be a good idea to disallow const parameters as parameters to traits entirely? This is the main way they'll show up in where clauses, and might be a rule that is easier to intuit and learn than that they can't be used in where clauses. Maybe we'll also have to disallow them in types used in where clauses, but that will be encountered less often. With traits the main reason to use a trait is to use it in where clauses, after all.

As you can see in the error, it complains about Inner, even thought the constant is of type Outer. Inner may be a private implementation detail, and you can't tell that something is structurally matchable recursively except by the way you described.

oh, I didn't even think about that at all so far.

Yea, for the MVP definitely, I don't see an immediate large scale thing that they would unlock.

After talking to lcnr and eddyb, I have updated the proposal for the MVP. Feedback again requested.

Which types a const parameter can be

In the MVP, we will only allow the integral primitives to be const parameters: signed and unsigned integer types, booleans, and chars.

Extending to all types with structural equality would be a near-term extension after the MVP.

Which expressions can be applied as a const parameter

In the MVP, const parameters can only contain these two kinds of expressions:

  1. Const parameters as identity expressions - that is if N is a free const parameter, it can only be applied as a const parameter as the expression N.
  2. Const expressions containing no free type or const parameters.

We will implement this behavior by refusing to resolve const and type parameters when computing a const parameter expression, except to implement the first case.

Which locations a const parameter can be used in

There will be no limitation on where const parameters can be used - they can be used on any item a type parameter could be used on, and in constraints, and so on.

We had a good discussion about this in zulip and determined that it isn't necessary. Basically, without lazy normalization there are two problems. First, users sometimes get bizarre cycle errors that are actually incorrect, and lazy normalization will resolve correctly. Second, the current implementation actually allows some code that should not be allowed, because it resolves an associated item that should be ambiguous. This means lazy normalization will break some hypothetical code. For example:

trait Trait<T> {
    const ASSOC_CONST: usize = 0;

impl Trait<()> for u8 {}

// `u8::ASSOC_CONST` is resolved today, but will be ambiguous
// under lazy normalization
type Foo<T, U>
     where u8: Trait<T> + Trait<U>
= [(T, U); u8::ASSOC_CONST];

This is not great. However, these examples are not easy to hit under the restrictions we've identified above, are basically pathological, and (importantly) are already allowed on stable because we allow consts in all these positions on stable by way of allowing arrays in all these positions.

So we will not limit where consts are allowed, but we will also not block const generics on lazy normalization.


Does that mean that the eventual stabilized implementation of Lazy Normalization will then resolve potentially working code as ambiguous or does that mean that when Lazy Normalization is stabilized that issue will have been resolved first? In other words, is it believed that that problem has a workable solution eventually?

The sample should correctly be understood as ambiguous because multiple definitions of u8::ASSOC_CONST are in scope via the where clauses, but the compiler today incorrectly treats it as unambiguous. It's always possible to disambiguate examples like this by defining the full path (for example <u8 as Trait<()>>::ASSOC_CONST).


We would still need an WF constant value check that enforces the value is actually an integer (and not e.g. a CTFE "abstract memory address", or anything weirder). Presumably some of that is already enforced by the current implementation for integer types, but not ADTs?

To be clear, as a consequence of the above, would the following code be expected to work in the MVP?

trait K<const N: usize> {
  const ASSOC: usize = N;
// ...
fn foo<const N: usize, T: K<N>>() -> [u8; T::ASSOC];

Based on the above restrictions this seems to be the most complicated code allowed right now, but it can't hurt to check...

No. T::ASSOC would not be allowed in that position.

That doesn't work at all (see playground), due to #68436 which we don't have a solution yet.

(In short, there's no way for the compiler to know that T::ASSOC will always be a valid array length, it could just as well panic - because of that, the only constant expressions in types that can successfully "use" generics must not actually depend on said generics, for now, and the MVP would be even more restricted)

Oh, right... I forgot about that. =(

Does this affect only arrays (and types that depend on the constant to compute their layout) or does it also affect e.g.

struct L<const N: usize>;
fn foo<const N: usize, T: K<N>>() -> L<T::ASSOC>;

Mind, the "T::ASSOC is not allowed in that position" doesn't seem to immediately follow from the proposed subset above (i.e., the paragraphs after "There will be no limitations..."). I think the mention of associated constants in the code snippet above is confusing things. (Maybe it's worth clarifying that this is still not allowed?)

The restrictions are imposed by "Which expressions can be applied as a const parameter", which also apply to the length expressions of array types (this could/should be clarified in the MVP proposal).

T::ASSOC does not satisfy the "no free type or const parameters" requirement of "2. Const expressions containing no free type or const parameters.", and the only other kind of allowed expression is a plain const parameter (N), which it's clearly not.

To answer to your second example: yes, that also doesn't work - it's not about layout, but rather the well-formedness ("WF") of the type, and statically knowing that the expression will evaluate (before being able to, due to e.g. unknown types like T in T::ASSOC).

I guess "no limitation" in the paragraph below is confusing?

Maybe "used" is the wrong word? I believe this is referring to where const parameters can be declared, i.e. write const N: ... between <...> (and the answer is that they can be declared in all the same places type parameters can).

1 Like

I chose the sort of ambiguous "use" because there had initially been a concern about needing to block const params from appearing in types or traits used in where clauses.

1 Like

I think that the criteria for which expressions can be applied as const parameters is good here, specifically in the context of an MVP. It would be a win just by allowing the standard library to implement traits for any sized array.

Most of the interesting things that are necessary for simple linear algebra libraries are covered by this. I try to use this library as much as possible in order to beta-test const generics as much as possible. I have recently run into a build failure upon upgrading to a new nightly, as certain const expressions in type expressions became illegal:, which manifests as the error:

error: constant expression depends on a generic parameter
   --> src/
173 |     pub fn truncate(self) -> (TruncatedVector<T, { N }>, T) {
    |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    = note: this may fail depending on what value the parameter takes

Although it would be nice to be able to express something like this eventually but it would require an update to the current const generics RFC. Additionally, the error message is very clear in indicating what is allowed and not allowed in const generic expressions, and it aligns with WithoutBoat's description of the MVP. Therefore, I think it is suitable for stabilization.


That's what boats' comment made me suspect. Thanks for clarifying. =)

This already exists, even for aggregates. The only thing where it doesn't work is through references to other statics, we don't follow references that point into statics. But since the static must be WF itself, you need unsafe code to construct a bad reference. Also constants can't refer to statics, so this is a non-issue for const generics.

const FOO: usize = unsafe { std::mem::transmute(&42) };

will give an error that can't be worked around

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.