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
- Semantic constraints in types โ `NonEmpty<T, 3>` is not just `[T; 3]`; it carries the proof that it's non-empty; methods like `first()` are safe without `Option` return types.
- Power-of-two fast modulo โ `i & (N - 1)` instead of `i % N` is only correct when `N` is a power of two; the where bound makes the optimization safe by preventing misuse.
- Alignment guarantees at the type level โ `AlignAtLeast<T, 16>` cannot be instantiated with a type whose natural alignment is less than 16; useful for SIMD buffers and DMA regions.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Type-level natural constraints | GADTs + phantom types | `where [(); expr]: Sized` โ const expression trick |
| Non-zero type | Existential with private constructor | `NonEmpty<T, N> where [(); N - 1]: Sized` |
| Power-of-two enforcement | Runtime `assert` | Compile-time via `assert!` in const block in where clause |
| Nightly improvements | N/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)