rustcompile-time

Limit parameter values at compile time


Is there a way in Rust to limit paramter values to a range or condition depending on a generic type?

Given a T as unsigned, u8 or u16

const fn do_something_with_a_bit_offset<T>(offset: u8) -> T {
    // .. some ops return a T ..
}

If T is u8, offset can be 0..7, offset > 7 should not compile
If T is u16, offset can be 0..15, offset > 15 should not compile

    let mask = do_something_with_a_bit_offset::<u8>(0);  // OK
    let mask = do_something_with_a_bit_offset::<u8>(8);  // Compile error
    let mask = do_something_with_a_bit_offset::<u16>(0);  // OK
    let mask = do_something_with_a_bit_offset::<u16>(8);  // OK
    let mask = do_something_with_a_bit_offset::<u16>(16);  // Compile error

How can I do this at compile time?


Solution

  • Before we start, I must mention the obvious: if offset is not a compile-time constant, there is no way to avoid a runtime check.

    If the offset parameter is guaranteed to be a compile-time constant, you can use const generics and some associated constant magic:

    trait CheckOffset<const OFFSET: usize> {
        const CHECKED_OFFSET: usize;
    }
    
    impl<T: MaxOffset, const OFFSET: usize> CheckOffset<OFFSET> for T {
        const CHECKED_OFFSET: usize = if OFFSET <= T::MAX_OFFSET {
            OFFSET
        } else {
            panic!("Invalid offset")    // Compile time panic
        };
    }
    

    CheckOffset is a helper trait that defines an associated constant CHECKED_OFFSET which (in its implementation) actually does the offset check. A reference to <T as CheckOffset<OFFSET>>::CHECKED_OFFSET will compile only if the given offset OFFSET is less than the maximum offset for T (<T as MaxOffset>::MAX_OFFSET). MaxOffset is a helper trait that defines the maximum bit offset for a type:

    trait MaxOffset {
        const MAX_OFFSET: usize;
    }
    
    impl<T> MaxOffset for T {
        const MAX_OFFSET: usize = 8 * size_of::<T>() - 1;
    }
    

    If you need a function to take in a compile-time checked offset for a given type T, you must

    1. Add a const generic argument (OFFSET) to represent the input offset
    2. Add a CheckOffset<OFFSET> constraint to the type T
    3. Use <T as CheckOffset<OFFSET>>::CHECKED_OFFSET as the actual offset. If you do not use the CHECKED_OFFSET constant, the max offset check in the associated constant implementation will not be evaluated.
    fn foo<T: CheckOffset<OFFSET> + 'static, const OFFSET: usize>() {
        let offset = T::CHECKED_OFFSET;
        println!("{} Offset: {:?}", type_name::<T>(), offset);
    }
    
    fn main() {
        foo::<u8, 0>();
        foo::<u8, 7>();
        // foo::<u8, 8>();      // Compile error
    
        foo::<u16, 0>();
        foo::<u16, 15>();
        // foo::<u16, 16>();    // Compile error
    
        foo::<u32, 0>();
        foo::<u32, 31>();
        // foo::<u32, 32>();    // Compile error
    }
    

    This approach will work for any compile-time const-evaluatable constraint you may have.

    Playground