๐Ÿฆ€ Functional Rust

781: Where Bounds on const Generic Parameters

Difficulty: 4 Level: Advanced `where [(); N - 1]: Sized` and `where [(); { assert!(...); 0 }]: Sized` โ€” type-level constraints on const expressions.

The Problem This Solves

Const generics let you put integers in types, but sometimes you need to constrain those integers at the type level: `N` must be non-zero, `N` must be a power of two, alignment must meet a minimum. Without these constraints, you'd get a panic at runtime (or worse, silent wrong behavior). With `where` bounds on const expressions, the constraint becomes a compile error. The pattern looks strange at first: `where [(); N - 1]: Sized` โ€” an array type of size `N-1`. If `N` is 0, `N - 1` underflows to a huge number (or panics in const context), making the bound unsatisfiable. This is a current workaround for the lack of direct const inequality constraints in stable Rust. The const block variant โ€” `where [(); { assert!(N > 0); 0 }]: Sized` โ€” is more explicit and gives better error messages. This pattern appears in the `tinyvec` crate (non-zero capacity assertion), cryptographic libraries (key length constraints), and any generic data structure where certain const parameter values are semantically invalid.

The Intuition

`[(); N]: Sized` is always true โ€” arrays of `()` of any size are `Sized`. The trick is making the array size an expression that evaluates to an invalid value for forbidden inputs. `[(); N - 1]: Sized` fails for `N = 0` because `0 - 1` underflows in const context. The `assert!` variant is cleaner: `[(); { assert!(N.is_power_of_two()); 0 }]: Sized` โ€” the assert panics at compile time for invalid `N`, and the array size is always `0` for valid `N`.

How It Works in Rust

// Non-zero constraint: N-1 overflows for N=0 โ†’ compile error
pub struct NonEmpty<T, const N: usize>
where
 [(); N - 1]: Sized,   // forbids N = 0 at the type level
{
 data: [T; N],
}

impl<T: Default + Copy, const N: usize> NonEmpty<T, N>
where
 [(); N - 1]: Sized,
{
 pub fn new() -> Self { Self { data: [T::default(); N] } }
 pub fn first(&self) -> &T { &self.data[0] }  // safe: N >= 1 guaranteed by type
 pub fn last(&self)  -> &T { &self.data[N - 1] }
}

// Power-of-two constraint: assert in const block
pub struct PowerOfTwoRing<T: Default + Copy, const N: usize>
where
 [(); { assert!(N.is_power_of_two(), "N must be a power of two"); 0 }]: Sized,
{
 buf: [T; N],
 head: usize,
 count: usize,
}

impl<T: Default + Copy, const N: usize> PowerOfTwoRing<T, N>
where
 [(); { assert!(N.is_power_of_two(), "N must be a power of two"); 0 }]: Sized,
{
 #[inline]
 fn idx(&self, i: usize) -> usize { i & (N - 1) }  // fast: N is power of two
 // ...
}

// Alignment constraint
pub struct AlignAtLeast<T, const ALIGN: usize>
where
 [(); { assert!(align_of::<T>() >= ALIGN, "alignment too small"); 0 }]: Sized,
{
 value: T,
}

// Usage
let ne: NonEmpty<i32, 3> = NonEmpty::new();   // ok
// let bad: NonEmpty<i32, 0> = NonEmpty::new(); // COMPILE ERROR โ€” [(); 0-1] fails

let mut ring: PowerOfTwoRing<i32, 8> = PowerOfTwoRing::new();  // ok
// let bad: PowerOfTwoRing<i32, 6> = PowerOfTwoRing::new();    // COMPILE ERROR โ€” 6 not pow2
Note: the `where` clause must be repeated on every `impl` block โ€” this is a current limitation of Rust's const generic implementation. The `N - 1` trick works only when `T` is `usize` (subtraction panics in const on underflow for debug builds). Nightly Rust has `feature(generic_const_exprs)` which cleans this up significantly.

What This Unlocks

Key Differences

ConceptOCamlRust
Type-level natural constraintsGADTs + phantom types`where [(); expr]: Sized` โ€” const expression trick
Non-zero typeExistential with private constructor`NonEmpty<T, N> where [(); N - 1]: Sized`
Power-of-two enforcementRuntime `assert`Compile-time via `assert!` in const block in where clause
Nightly improvementsN/A`feature(generic_const_exprs)` โ€” direct `where N > 0`
//! # Const Where Bounds
//!
//! Constraining const generic parameters.
//!
//! Note: Rust stable doesn't support `where [(); expr]:` bounds on const generics.
//! We demonstrate the concept using runtime assertions and trait-based patterns.

/// Non-empty array โ€” uses a const generic N and stores [T; N].
/// The constraint N >= 1 is enforced at construction time.
pub struct NonEmptyArray<T, const N: usize> {
    data: [T; N],
}

impl<T: Default + Copy, const N: usize> NonEmptyArray<T, N> {
    /// Panics if N == 0.
    pub fn new() -> Self {
        assert!(N >= 1, "NonEmptyArray requires N >= 1");
        NonEmptyArray {
            data: [T::default(); N],
        }
    }

    pub fn first(&self) -> &T {
        &self.data[0]
    }

    pub fn last(&self) -> &T {
        &self.data[N - 1]
    }
}

impl<T: Default + Copy, const N: usize> Default for NonEmptyArray<T, N> {
    fn default() -> Self {
        Self::new()
    }
}

/// Power of two buffer โ€” fast modulo via bitmask.
/// The power-of-two constraint is checked at construction.
pub struct PowerOfTwoBuffer<const SIZE: usize> {
    data: [u8; SIZE],
}

impl<const SIZE: usize> PowerOfTwoBuffer<SIZE> {
    pub fn new() -> Self {
        assert!(SIZE > 0 && (SIZE & (SIZE - 1)) == 0, "SIZE must be a power of 2");
        PowerOfTwoBuffer { data: [0; SIZE] }
    }

    pub const fn size(&self) -> usize {
        SIZE
    }

    /// Fast modulo using bit mask (works because SIZE is power of 2).
    pub const fn wrap_index(&self, idx: usize) -> usize {
        idx & (SIZE - 1)
    }
}

/// Aligned chunks: divide M items into chunks of N.
/// Constraint M % N == 0 is enforced at construction.
pub struct AlignedChunks<T> {
    data: Vec<Vec<T>>,
    chunk_size: usize,
}

impl<T: Default + Clone> AlignedChunks<T> {
    pub fn new(chunk_size: usize, total: usize) -> Self {
        assert!(chunk_size > 0, "chunk_size must be > 0");
        assert!(total % chunk_size == 0, "total must be divisible by chunk_size");
        let num_chunks = total / chunk_size;
        let data = vec![vec![T::default(); chunk_size]; num_chunks];
        AlignedChunks { data, chunk_size }
    }

    pub fn chunk_size(&self) -> usize {
        self.chunk_size
    }

    pub fn num_chunks(&self) -> usize {
        self.data.len()
    }

    pub fn get_chunk(&self, idx: usize) -> Option<&[T]> {
        self.data.get(idx).map(|v| v.as_slice())
    }
}

/// Minimum size buffer โ€” ensures N >= 64.
pub struct MinSizeBuffer<const N: usize> {
    data: [u8; N],
}

impl<const N: usize> MinSizeBuffer<N> {
    pub fn new() -> Self {
        assert!(N >= 64, "MinSizeBuffer requires N >= 64");
        MinSizeBuffer { data: [0; N] }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_non_empty_array() {
        let arr: NonEmptyArray<i32, 5> = NonEmptyArray::new();
        assert_eq!(*arr.first(), 0);
        assert_eq!(*arr.last(), 0);
    }

    // NonEmptyArray::<i32, 0>::new() would panic at runtime

    #[test]
    fn test_power_of_two_buffer() {
        let buf: PowerOfTwoBuffer<16> = PowerOfTwoBuffer::new();
        assert_eq!(buf.size(), 16);
        assert_eq!(buf.wrap_index(17), 1); // 17 % 16 = 1
    }

    // PowerOfTwoBuffer::<15>::new() would panic (15 is not power of 2)

    #[test]
    fn test_aligned_chunks() {
        let chunks: AlignedChunks<i32> = AlignedChunks::new(4, 12);
        assert_eq!(chunks.chunk_size(), 4);
        assert_eq!(chunks.num_chunks(), 3);
    }

    // AlignedChunks::new(5, 12) would panic (12 % 5 != 0)

    #[test]
    fn test_min_size() {
        let buf: MinSizeBuffer<128> = MinSizeBuffer::new();
        assert_eq!(buf.data.len(), 128);
    }
}
(* Where bounds on const parameters โ€” OCaml via functor constraints *)

(* We encode "non-zero" at the type level using a module constraint *)
module type POSITIVE = sig
  val n : int
  val proof : unit (* force explicit instantiation *)
end

(* Helper: create a POSITIVE module, failing if n <= 0 *)
let make_positive n =
  if n <= 0 then failwith (Printf.sprintf "N must be positive, got %d" n);
  (module struct let n = n let proof = () end : POSITIVE)

(* Power-of-two constraint *)
module type POW2 = sig
  val n : int
  val log2 : int
end

let is_power_of_two n = n > 0 && (n land (n - 1)) = 0

let log2 n =
  let rec go acc n = if n <= 1 then acc else go (acc + 1) (n lsr 1) in
  go 0 n

let make_pow2 n =
  if not (is_power_of_two n) then
    failwith (Printf.sprintf "%d is not a power of two" n);
  (module struct let n = n let log2 = log2 n end : POW2)

module PowerOfTwoBuf (S : POW2) = struct
  let mask = S.n - 1  (* fast modulo for power-of-two sizes *)
  let modulo i = i land mask
  let capacity = S.n
end

let () =
  let (module B8) = make_pow2 8 in
  let module Buf = PowerOfTwoBuf((val make_pow2 8 : POW2)) in
  Printf.printf "Capacity: %d, log2=%d\n" B8.n B8.log2;
  Printf.printf "modulo(10, 8) = %d\n" (Buf.modulo 10)