rust icon indicating copy to clipboard operation
rust copied to clipboard

Tracking issue for future-incompatibility lint `const_evaluatable_unchecked`

Open lcnr opened this issue 5 years ago • 23 comments

This is the summary issue for the CONST_EVALUATABLE_UNCHECKED future-compatibility warning. The goal of this page is to describe why this change was made and how you can fix code that is affected by it. It also provides a place to ask questions or register a complaint if you feel the change should not be made. For more information on the policy around future-compatibility warnings, see our breaking change policy guidelines.

What is the warning for?

In version 1.43 we accidentally allowed some uses of generic parameters in repeat expressions. This has a few problems which are outlined in the following document for now.

When will this warning become a hard error?

Most probably after const well-formedness bounds get stabilized, which is currently not yet implemented. There might be a subset of cases here which we can allow without needing any changes, but these require further careful consideration.

How to fix this?

As we only allowed generic constants where the actual type did not matter, it should always be possible to replace the generic parameters with dummy types. For example [0; std::mem::size_of::<*mut T>()] can be changed to [0; std::mem::size_of::<*mut ()>()].

Please report your case here so we may take it into consideration when deciding on how to proceed.

lcnr avatar Sep 01 '20 14:09 lcnr

Please report your case here

Merging #74532 just now ran into this problem. I worked around it by using () as dummy type:

-        let [] = [(); align_of::<Self>() - align_of::<*mut T>()];
+        let [] = [(); align_of::<AtomicPtr<()>>() - align_of::<*mut ()>()];

Here, not only *mut T was a problem, but Self (which is AtomicPtr<T>) was a problem as well.

m-ou-se avatar Sep 12 '20 19:09 m-ou-se

Following from https://users.rust-lang.org/t/cant-understand-warning-when-using-const-evaluatable-checked/55630/6?u=yandros, the following snippet causes three warnings associated with the lint of this tracking issue, and a hard error:

const _10: usize = 10;

fn write_struct<T> ()
where
    can_be_evaluated!(_10 - 0):,
{
    foo::<T, 10>(); // Warning
    foo::<T, { _10 }>(); // Error (same for `{ 10 }`)
    WithConst::<10>::foo::<T>(); // Warning
    WithConst::<{ _10 }>::foo::<T>(); // Warning
    
    WithConst::<10>::write_struct::<T>(); // OK
    WithConst::<{ _10 }>::write_struct::<T>(); // OK
}

where foo in both cases features, for some generic constant N:

  • some non-generic type with an evaluatable constraint / that depends on a(n assumed) fallible computation of N (N - 0 in the examples; note that I am not talking about N - 0 being infallible, but rather, of a computation that does not fail for a concrete value of N (10 in the example));

  • an unrelated (to the above type) type parameter T.

In the case of WithConst, I am using a helper type to introduce N (and the evaluatable bound!) clearly before the generic type T is ever introduced, to ensure the evaluatable bound cannot possibly depend on T.

As you can see, a caller that:

  • forwards the generic type parameter,

  • but using a concrete constant parameter (one that does verify the evaluatable bound),

now triggers either this warning, or some related error when the constant is braced (which is sadly required when using a non-literal constant; even when the function features an evaluatable bound on said non-literal constant).

Finally, the last two calls in this example are interesting: by moving the logic of the function within the generic-and-evaluatable-bounded impl block for WithConst, we can later introduce that <T> type parameter without issues, and we can then call that helper function from outside with a fixed constant and it doesn't trigger a warning whatsoever.


This leads to two questions:

  • is this warning behavior really intended to affect these things?

  • And what about the workaround: is that intended too?

danielhenrymantilla avatar Feb 16 '21 15:02 danielhenrymantilla

From what I can tell this is not expected, is_const_evaluatable seems to be incorrect here if feature(const_evaluatable_checked) is active.

Afaict all of the examples you've given should work without warnings or errors.

lcnr avatar Feb 26 '21 10:02 lcnr

Please report your case here

#![feature(generic_const_exprs)]

fn foo<T: Default + Copy, const L: usize>(_: [T; L]) -> [T; L + 1] {
    [T::default(); L + 1]
}

fn bar(x: [u8; 2]) -> [u8; 3] {
    foo::<u8, 2>(x)
}

fn baz<T: Default + Copy>(x: [T; 2]) -> [T; 3] {
    foo::<T, 2>(x)
}

fn main() {}

In 1.57.0-nightly (e4828d5b7 2021-09-16) baz produces warning: cannot use constants which depend on generic parameters in types. bar does not.

is8ac avatar Sep 17 '21 15:09 is8ac

While checking whether the fixed crate can be ported from typenum to nightly with generic_const_exprs, I hit this warning. I am not sure whether it's a false positive or whether the behavior I want is meant to be disallowed, but I think it should be allowed when enabling generic_const_exprs.

I've reduced the code though I left some stuff to show why it would be useful.

#![feature(generic_const_exprs)]

pub struct If<const CONDITION: bool>;
pub trait True {}
impl True for If<true> {}

pub struct FixedI8<const FRAC: u32> {
    pub bits: i8,
}

impl<const FRAC_LHS: u32, const FRAC_RHS: u32> PartialEq<FixedI8<FRAC_RHS>> for FixedI8<FRAC_LHS>
where
    If<{ FRAC_RHS <= 8 }>: True,
{
    fn eq(&self, _rhs: &FixedI8<FRAC_RHS>) -> bool {
        unimplemented!()
    }
}

impl<const FRAC: u32> PartialEq<i8> for FixedI8<FRAC> {
    fn eq(&self, rhs: &i8) -> bool {
        let rhs_as_fixed = FixedI8::<0> { bits: *rhs };
        PartialEq::eq(self, &rhs_as_fixed)
    }
}

tspiteri avatar Feb 23 '22 09:02 tspiteri

@tspiteri this is a bug which only happens with feature(generic_const_exprs), opened #94293 to track that

lcnr avatar Feb 23 '22 14:02 lcnr

I admit not understanding this (and reasoning behind it) fully. But I have use case for an associative type with a bound that depends on its owner (trait)'s const generic parameter. Any alternatives/workarounds, please?

Short example failing no Nightly (thanks to ilyvion#9946 for narrowing it down): https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=c133c09b2fa155bb3f4b9ee45e6c011e.

My use case: https://github.com/ranging-rs/slicing-rs/blob/main/src/slices/mod.rs#L30=. I know it's a semantic/voluntary-like constraint only, but I believe it deserves its place. (Feel free to follow up at https://github.com/ranging-rs/slicing-rs/issues/1.)

peter-lyons-kehl avatar May 15 '22 20:05 peter-lyons-kehl

@peter-kehl this is #94293 again, this lint is only relevant when not using feature(generic_const_exprs)

lcnr avatar May 16 '22 06:05 lcnr

I don't really understand the rationality of this limitation, also, it's weird to need to read it on external link and not directly in github. I think this subject didn't get much attention.

Recent question where an user would want to use it How can I give a name to the constant size of an array which is a member of a struct?

Stargateur avatar May 01 '23 13:05 Stargateur

Disclaimer: I'm neither a compiler engineer, nor a typesystem-language-lawyer – I'm sure, from a technical point of view, the rationale behind this is probably sound.

However, if I look at this from the developer-UX-perspective, I'm not happy with this warning...

Specific use-case

I have a struct (simplified):

/// Raw SPI command interface for RFM95
pub struct Rfm95<Peripheral, Tx, Rx, Sck, Select, Reset>
where
    Peripheral: SpiDevice,
    Tx: PinId + ValidPinIdTx<Peripheral>,
    Rx: PinId + ValidPinIdRx<Peripheral>,
    Sck: PinId + ValidPinIdSck<Peripheral>,
    Select: PinId,
    Reset: PinId,
{
    /// The SPI bus
    bus: SpiBus<Peripheral, Tx, Rx, Sck>,
    /// The chip select line
    select: PinChipSelect<Select>,
    /// The chip reset line
    reset: PinReset<Reset>,
}
impl<Peripheral, Tx, Rx, Sck, Select, Reset> Rfm95<Peripheral, Tx, Rx, Sck, Select, Reset>
where
    Peripheral: SpiDevice,
    Tx: PinId + ValidPinIdTx<Peripheral>,
    Rx: PinId + ValidPinIdRx<Peripheral>,
    Sck: PinId + ValidPinIdSck<Peripheral>,
    Select: PinId,
    Reset: PinId,
{
    /// The total FIFO size
    const FIFO_SIZE: usize = 0xFF;
    
    /// The register address to access the FIFO address pointer
    const REGISTER_FIFO_ADDRESS_POINTER: u8 = 0x0D;

    // ... implementation and member functions ...
}

Now, as naive developer, I tried to create an array in a member function

// Copy data into mutable buffer
let mut data_mut = [0; Self::FIFO_SIZE];

which raises this warning. I really don't understand why this restriction exists and this warning is emitted... the constant is fully constant; it's not dependent on any of the generic types, has no dynamic parts whatsoever, it's always 0xFF – it's so 100% boring, it could even be filled in using C-macro-style via search-replace.

Coming from a naive developer-UX-perspective, it's really not understandable why that's a problem at all. Now I'm sitting here without any reasonable workaround* (except free-floating constants or magic values), and that was so unsatisfying, I wrote this comment here 😅 IMO, this warning should either be dropped until there's a reasonable workaround; or the lint should link to/propose a reasonable workaround.

*If I missed something, please tell me and I'll happily heat my own words 😅

KizzyCode avatar May 07 '24 01:05 KizzyCode

I think the intended work-around is to move the const FIFO_SIZE out of the impl block. By declaring it inside the impl block, your const implicitly becomes dependent on all generic parameters of the type, and that's what triggers the warning.

RalfJung avatar May 07 '24 06:05 RalfJung

By declaring it inside the impl block, your const implicitly becomes dependent on all generic parameters of the type, and that's what triggers the warning.

I don't really understand this part. As far as I see it, in most cases I can declare and use constants within generic structs pretty fine. I don't get a warning if I use FIFO_SIZE in conditional checks or REGISTER_FIFO_ADDRESS_POINTER; the only problem where this doesn't work is for array sizes and stuff.

And that feels inconsistent.

I think the intended work-around is to move the const FIFO_SIZE out of the impl block

Yup, it's the obvious fix, but that also feels more like a hack. Now I have to move this single const out of its associated context, while all other consts can stay in there and be used for constant operations (even compile-time assertions). At that point, I might as well hardcode the constant 😅

EDIT:

It appears even more inconsistent once you realize that you can easily do stuff like

impl<Peripheral, Tx, Rx, Sck, Select, Reset> Rfm95<Peripheral, Tx, Rx, Sck, Select, Reset>
where
    Peripheral: SpiDevice,
    Tx: PinId + ValidPinIdTx<Peripheral>,
    Rx: PinId + ValidPinIdRx<Peripheral>,
    Sck: PinId + ValidPinIdSck<Peripheral>,
    Select: PinId,
    Reset: PinId,
{
    /// A default FIFO buffer "seed"
    const FIFO_BUF_DEFAULT: [u8; 0xFF] = [0x00; 0xFF];
}

and use that instead, which works again.


Please note that I don't want to flame here; it's just that Rust makes great effort to be intuitive and developer-friendly, from error messages to 0-compromise correct APIs (e.g. Path). This one however feels out of place ("unrusty"), and reminds me more of C++-template magic and unexpected errors. So I thought this might be worth giving some feedback.

KizzyCode avatar May 11 '24 23:05 KizzyCode

I don't really understand this part. As far as I see it, in most cases I can declare and use constants within generic structs pretty fine. I don't get a warning if I use FIFO_SIZE in conditional checks or REGISTER_FIFO_ADDRESS_POINTER; the only problem where this doesn't work is for array sizes and stuff.

Correct; using them as values is fine, but using them in types (like in an array size) is problematic. There's a big difference between these two ways of using something. That should hopefully be less surprising if you consider that runtime values are entirely forbidden as array sizes! Just because consts are allowed in both places, doesn't mean that suddenly these fundamental differences disappear. Consts are allowed in both places but they behave quite differently.

Now, this is all preliminary, and I don't know the long-term plans for generic consts. But there is an underlying consistency here. The confusing stems from conflating runtime expressions and compile-time expressions even though those are completely different beasts.

RalfJung avatar May 12 '24 06:05 RalfJung

ICU4X hits this here:

https://github.com/unicode-org/icu4x/blob/fcef14a31dbe5e2d0ee5f2a489872df373c2d0de/utils/zerotrie/src/options.rs#L135-L138

We use Self::FLAGS in a bunch of match statements, so it must be a constant.

This const is not dependent on the generic parameter.

Manishearth avatar Oct 11 '24 20:10 Manishearth

Correct; using them as values is fine, but using them in types (like in an array size) is problematic. There's a big difference between these two ways of using something. That should hopefully be less surprising if you consider that runtime values are entirely forbidden as array sizes! Just because consts are allowed in both places, doesn't mean that suddenly these fundamental differences disappear. Consts are allowed in both places but they behave quite differently.

Now, this is all preliminary, and I don't know the long-term plans for generic consts. But there is an underlying consistency here. The confusing stems from conflating runtime expressions and compile-time expressions even though those are completely different beasts.

I think what bugs me here is that it's obvious for my brain that this value is "fully constant". FIFO_SIZE should always be 0xFF, as it does not depend on the generic parameter – if I don't miss anything, there is no way this code could ever compile or specialize to something else then 0xFF in current Rust.

And if that assumption is true, IMO this should be treated as a special case that is "fully constant", and should not fail. Of course, this is from my developer-UX "consumer" perspective – I don't know how hard it is to make the compiler prove that case, or if it would block future language development 😅

(Sorry for the late reply; I somehow accidentally unsubscribed here.)

KizzyCode avatar Feb 17 '25 15:02 KizzyCode

I guess there is some regression, as it warned this piece of code (warning and code are redacted):

        let mut buf = [0u8; const { size_of::<::foo::bar::Baz>() }];
warning: cannot use constants which depend on generic parameters in types
  --> <redacted>:38:29
   |
38 |         let mut buf = [0u8; const { size_of::<::foo::bar::Baz>() }];
   |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #76200 <https://github.com/rust-lang/rust/issues/76200>
   = note: `#[warn(const_evaluatable_unchecked)]` on by default

It clearly does not use any generics.

Ddystopia avatar Feb 28 '25 12:02 Ddystopia

@Ddystopia inline consts desugar to an item that uses all in scope generic parameters, you should remove the const block there as it does not accomplish anything, if you need braces around an array length simply writing { ... } should work fine. no need for const { ... }

BoxyUwU avatar Mar 06 '25 23:03 BoxyUwU

Ran into this warning and wanted to share my confusion with it. My confusion stems from the fact that this warning seems to be triggered inconsistently. Example of this

pub struct Example<const CONSTANT: u32>;

impl<const CONSTANT: u32> Example<CONSTANT> {
    const DOESNT_CHANGE: usize = 3;
    const ALSO_DOESNT_CHANGE: usize = Self::DOESNT_CHANGE;
    
    pub fn does_trigger_warning(&self, i: usize) -> usize {
        match i {
            Self::ALSO_DOESNT_CHANGE => 0,
            _ => 1
        }
    }
    
    pub fn doesnt_trigger_warning(&self, i: usize) -> usize {
        match i {
            Self::DOESNT_CHANGE => 0,
            _ => 1
        }
    }
    
    pub fn also_doesnt_trigger_warning(&self, i: usize) -> usize {
        if i == Self::ALSO_DOESNT_CHANGE {
            0
        } else {
            1
        }
    }
}

The other thing that I found confusing was that the warning shows up on the declaration of DOESNT_CHANGE and not the usage (which is what caused the warning to appear).

I am using cargo 1.88.0-nightly (d811228b1 2025-04-15) and rustc 1.88.0-nightly (077cedc2a 2025-04-19).

MorganBennetDev avatar Apr 28 '25 22:04 MorganBennetDev

@MorganBennetDev that seems like a bug to me. I don't expect us to make that an error when we fix the special casing of repeat expr counts. I split that out into #140447

BoxyUwU avatar Apr 29 '25 11:04 BoxyUwU

I've just hit this warning on current nightly in a project of mine, but I'm unsure the lint is actually supposed to fire here since there aren't any generics in scope.

The code (expanded) looked something like this:

static ON_BAR_CLICK: [f32; 2940] = const {
	const BYTES: &[u8] = include_bytes!("../../assets/on_bar_click.pcm");
	assert!(BYTES.len().is_multiple_of(4));
	let mut f32s = [0.0; const { BYTES.len() / 4 }];
	let mut i = 0;
	while i < f32s.len() {
		f32s[i] = f32::from_le_bytes([
			BYTES[4 * i],
			BYTES[4 * i + 1],
			BYTES[4 * i + 2],
			BYTES[4 * i + 3],
		]);
		i += 1;
	}
	f32s
};

Removing the const block around the array's length fixed the warning (https://github.com/generic-daw/generic-daw/commit/e1b6220ecc347aa43221ac41cfbf87252690166e), however I'm not sure the warning was actually supposed to fire here since there aren't any generics for the const block to capture, as far as I can tell.

edwloef avatar Nov 03 '25 10:11 edwloef

I'm a bit confused on whether the triggering of this warning is expected behaviour or not.

Code, Errors and Questions

```rust struct Identifier(heapless::String);

impl<const N: usize> Identifier<N> { const NAMESPACE: &'static str = "default"; }

impl<const N: usize> Decode for Identifier<N> { async fn decode(...) -> Result<Self> {

    ...

    let mut namespace_buf = [0u8; Self::NAMESPACE.len() + 1];

    ...
}

}


Results in the error:

warning: cannot use constants which depend on generic parameters in types --> src/identifier.rs:43:39 | 43 | ...f = [0u8; Self::NAMESPACE.len() + 1]; | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! = note: for more information, see issue #76200 https://github.com/rust-lang/rust/issues/76200


However, if I extract `Self::NAMESPACE.len()` into its own const, it results in no warning:

```rust

...
impl<const N: usize> Identifier<N> {
    const NAMESPACE: &'static str = "default";
    const NAMESPACE_LEN = Self::NAMESPACE.len();
}

impl<const N: usize> Decode for Identifier<N> {
    async fn decode(...) -> Result<Self> {
        ...
        let mut namespace_buf = [0u8; Self::NAMESPACE_LEN + 1];
        ...
    }
}

This even works if const NAMESPACE_LEN: usize = Identifier::<N>::NAMESPACE.len(). In both situations, NAMESPACE.len() does not depend on N, but the const's are impl's of a generic type.

If this is true:

By declaring it inside the impl block, your const implicitly becomes dependent on all generic parameters of the type, and that's what triggers the warning.

then neither of these implementations should work, but from a DX POV they should both work.

If I missing something here please do correct me.

Using cargo 1.91.1 (ea2d97820 2025-10-10)

Sycrosity avatar Dec 03 '25 00:12 Sycrosity

@Sycrosity can you provide a complete example where it does not lint?

lcnr avatar Dec 03 '25 08:12 lcnr

@lcnr I think I accidentally used Identifier::<1>::NAMESPACE_LEN() rather than Self::NAMESPACE_LEN() which I believe is the intended behaviour (though as others have mentioned, feels expected). I've provided the two implementations below just in case.

Code where the lint is triggered

impl<const N: usize> Identifier<N> {
    const NAMESPACE: &'static str = "default";
    

    pub fn new<S: Into<String<N>>>(s: S) -> Self {
        Identifier(s.into())
    }
}

impl<const N: usize> Encode for Identifier<N> {
    async fn encode<W: embedded_io_async::Write>(
        &self,
        mut buffer: W,
    ) -> Result<(), EncodeError<W::Error>> {

        
        VarInt(Self::NAMESPACE.len() as i32 + self.0.len() as i32)
            .encode(&mut buffer)
            .await?;
        buffer.write(Self::NAMESPACE.as_bytes()).await?;
        buffer.write_u8(b':').await?;
        buffer.write(self.0.as_bytes()).await?;
        Ok(())
    }
}

impl<const N: usize> Decode for Identifier<N> {
    async fn decode<R: embedded_io_async::Read>(
        mut buffer: R,
    ) -> Result<Self, DecodeError<R::Error>> {
        let length = *VarInt::decode(&mut buffer).await?;

        if length < Self::NAMESPACE.len() as i32 + 1 {
            return Err(DecodeError::VarIntTooSmall(VarInt(0)));
        }

        if length as usize > Self::NAMESPACE.len() + 1 + N {
            return Err(DecodeError::VarIntTooBig);
        }

        // #[allow(const_evaluatable_unchecked)]
        let mut namespace_buf = [0u8; Self::NAMESPACE.len() + 1];

        buffer.read_exact(&mut namespace_buf).await?;

        let mut path_buf = Vec::<u8, N>::new();

        let path_length = length as usize - Self::NAMESPACE.len() - 1;

        //the error from this shouldn't be possible as we've already checked the
        // length.
        path_buf
            .resize_default(path_length).expect("length already checked");

        buffer.read_exact(&mut path_buf).await?;

        Ok(Self(
            String::from_utf8(path_buf).map_err(DecodeError::InvalidUtf8)?,
        ))
    }
}
Code which doesn't lint

impl<const N: usize> Identifier<N> {
    const NAMESPACE: &'static str = "default";
    const NAMESPACE_LEN: usize = Self::NAMESPACE.len();

    pub fn new<S: Into<String<N>>>(s: S) -> Self {
        Identifier(s.into())
    }
}

impl<const N: usize> Encode for Identifier<N> {
    async fn encode<W: embedded_io_async::Write>(
        &self,
        mut buffer: W,
    ) -> Result<(), EncodeError<W::Error>> {

        
        VarInt(Self::NAMESPACE.len() as i32 + self.0.len() as i32)
            .encode(&mut buffer)
            .await?;
        buffer.write(Self::NAMESPACE.as_bytes()).await?;
        buffer.write_u8(b':').await?;
        buffer.write(self.0.as_bytes()).await?;
        Ok(())
    }
}

impl<const N: usize> Decode for Identifier<N> {
    async fn decode<R: embedded_io_async::Read>(
        mut buffer: R,
    ) -> Result<Self, DecodeError<R::Error>> {
        let length = *VarInt::decode(&mut buffer).await?;

        if length < Self::NAMESPACE.len() as i32 + 1 {
            return Err(DecodeError::VarIntTooSmall(VarInt(0)));
        }

        if length as usize > Self::NAMESPACE.len() + 1 + N {
            return Err(DecodeError::VarIntTooBig);
        }

        // #[allow(const_evaluatable_unchecked)]
        let mut namespace_buf = [0u8; Identifier::<1>::NAMESPACE_LEN + 1];

        buffer.read_exact(&mut namespace_buf).await?;

        let mut path_buf = Vec::<u8, N>::new();

        let path_length = length as usize - Self::NAMESPACE.len() - 1;

        //the error from this shouldn't be possible as we've already checked the
        // length.
        path_buf
            .resize_default(path_length).expect("length already checked");

        buffer.read_exact(&mut path_buf).await?;

        Ok(Self(
            String::from_utf8(path_buf).map_err(DecodeError::InvalidUtf8)?,
        ))
    }
}

Sycrosity avatar Dec 03 '25 12:12 Sycrosity