Question on `const-generics`

The compiler doesnt allow us to use a generic constant N (in the example below) inside a matched arm, in this use-case. I would've assumed that since we are matching on N and we're inside a matched arm, where N == 48, this should work.

Does the compiler not have access to this information at compile time?

#[derive(Debug, Clone, PartialEq)]
pub struct MyAffinePoint<const N: usize> {
    pub x: BigInt,
    pub y: BigInt,
    pub infinity: bool,
}

impl<const N: usize> MyAffinePoint<N> {
    /// Returns the base point of a NIST p-cURVE.
    pub fn generator() -> APTypes {
        match N {
            // NIST P-384 basepoint in affine coordinates:
            // x = aa87ca22 be8b0537 8eb1c71ef 320ad74 6e1d3b62 8ba79b98 59f741e0 82542a38 5502f25d bf55296c 3a545e38 72760ab7
            // y = 3617de4a 96262c6f 5d9e98bf9 292dc29 f8f41dbd 289a147c e9da3113 b5f0b8c0 0a60b1ce 1d7e819d 7a431d7c 90ea0e5f
            48 => {
                // Is this expected? The compiler cant seem to tell that the generic constant `N` equals `48`in a `matched` arm. I'm
                // assuming the compiler has access to this information at compile time.
                let x: [u8; N] = [
                    0xaa, 0x87, 0xca, 0x22, 0xbe, 0x8b, 0x05, 0x37, 0x8e, 0xb1, 0xc7, 0x1e, 0xf3,
                    0x20, 0xad, 0x74, 0x6e, 0x1d, 0x3b, 0x62, 0x8b, 0xa7, 0x9b, 0x98, 0x59, 0xf7,
                    0x41, 0xe0, 0x82, 0x54, 0x2a, 0x38, 0x55, 0x02, 0xf2, 0x5d, 0xbf, 0x55, 0x29,
                    0x6c, 0x3a, 0x54, 0x5e, 0x38, 0x72, 0x76, 0x0a, 0xb7,
                ];
                let y: [u8; 48] = [
                    0x36, 0x17, 0xde, 0x4a, 0x96, 0x26, 0x2c, 0x6f, 0x5d, 0x9e, 0x98, 0xbf, 0x92,
                    0x92, 0xdc, 0x29, 0xf8, 0xf4, 0x1d, 0xbd, 0x28, 0x9a, 0x14, 0x7c, 0xe9, 0xda,
                    0x31, 0x13, 0xb5, 0xf0, 0xb8, 0xc0, 0x0a, 0x60, 0xb1, 0xce, 0x1d, 0x7e, 0x81,
                    0x9d, 0x7a, 0x43, 0x1d, 0x7c, 0x90, 0xea, 0x0e, 0x5f,
                ];

                APTypes::P384(MyAffinePoint {
                    x: BigInt::from_bytes_be(Sign::Plus, &x),
                    y: BigInt::from_bytes_be(Sign::Plus, &y),
                    infinity: false,
                })
            }

            66 => APTypes::__Nonexhaustive,
            _ => APTypes::__Nonexhaustive,
        }
    }

Compiler throws the following error message -

error[E0308]: mismatched types
  --> src/crypto/affine_math.rs:55:34
   |
55 |                   let x: [u8; N] = [
   |  ________________________-------___^
   | |                        |
   | |                        expected due to this
56 | |                     0xaa, 0x87, 0xca, 0x22, 0xbe, 0x8b, 0x05, 0x37, 0x8e, 0xb1, 0xc7, 0x1e, 0xf3,
57 | |                     0x20, 0xad, 0x74, 0x6e, 0x1d, 0x3b, 0x62, 0x8b, 0xa7, 0x9b, 0x98, 0x59, 0xf7,
58 | |                     0x41, 0xe0, 0x82, 0x54, 0x2a, 0x38, 0x55, 0x02, 0xf2, 0x5d, 0xbf, 0x55, 0x29,
59 | |                     0x6c, 0x3a, 0x54, 0x5e, 0x38, 0x72, 0x76, 0x0a, 0xb7,
60 | |                 ];
   | |_________________^ expected `N`, found `48_usize`
   |
   = note: expected array `[u8; N]`
              found array `[u8; 48]`

error: aborting due to previous error

Yes, this is expected behavior. Doing this correctly in general would require refinement types, which is a large feature and likely won't be implemented any time soon.

Note that having access to informations at compile time doesn't mean the compiler is programmed to use them.

1 Like

Thank you. Appreciate it.

Quick follow-up - Refinement types is a large feature - Is there a discussion taking place about this feature? If yes, I'd be very interested in learning more.

PS- I've been trying to use const-generics to mimic refinement-types - https://bit.ly/3p0GrYD

No, I don't believe there is, but I may not be aware of discussion elsewhere.

I’ve used Any to do this sort of fake specialization for generic types before; looks like it will work for const generics as well:

fn array_cast<T, const N: usize, const M: usize>(arr: [T; N]) -> Option<[T; M]>
where
    T: 'static,
{
    let mut opt_arr = Some(arr);
    (&mut opt_arr as &mut dyn std::any::Any)
        .downcast_mut::<Option<[T; M]>>()
        .map(Option::take)
        .flatten()
}

fn cast_example<const N: usize>() -> Option<[u32; N]> {
    array_cast::<u32, 5, N>([1, 2, 3, 4, 5])
}

(Playground)

If you want to elide the 'static bound and the "expensive" checks associated with Any, you will need to use unsafe

fn array_cast<T, const N: usize, const M: usize>(arr: [T; N]) -> Option<[T; M]> {
    if N == M {
        use std::mem::{ManuallyDrop, transmute_copy};
        Some(unsafe { transmute_copy(&ManuallyDrop::new(arr)) })
    } else {
        None
    }
}
1 Like

Every time I’ve looked at the optimizer output for this sort of code, it’s managed to get rid Any’s internal checks on its own. That’s of course no guarantee it’ll always happen, though.

I also haven’t ever done this trick with const parameters in place of type parameters before.

3 Likes

That's interesting! I didn't think that would happen. Usually I don't see LLVM devirtualizing function calls, which is why I mentioned the unsafe version. But if it is handled, then that's better.

It’s a particularly easy case for the compiler to figure out, I think: Both TypeIds it’s comparing are hardcoded into array_cast during monomorphization— The optimizer statically knows the contents of the vtable because it’s created right here.

EDIT: Looks like the compiler can figure this one out, too: (Godbolt)

EDIT2: Actually, there’s no vtable lookup at all. downcast_mut is an inherent method of the trait object dyn Any.

1 Like

However under the hood it calls type_id which is a method on Any, so it still needs a vtable lookup

2 Likes