782-const-eval-patterns — Const Eval Patterns
Tutorial Video
Text description (accessibility)
This video demonstrates the "782-const-eval-patterns — Const Eval Patterns" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Compile-time evaluation is not limited to simple arithmetic. Key difference from OCaml: 1. **FNV at compile time**: Rust computes FNV hashes at compile time for switch dispatch; OCaml computes at runtime (module initialization), which is still fast but not embedded in the binary.
Tutorial
The Problem
Compile-time evaluation is not limited to simple arithmetic. Complex computations — string hashing for perfect hash maps, computing bit-reversal permutations, generating code tables — can run during compilation. This example collects practical const fn patterns: FNV hash of a string, log2 of a size for bit-counting, compile-time min/max/clamp, and ANSI escape code generation for terminal colors. These patterns eliminate entire categories of runtime initialization.
🎯 Learning Outcomes
const fn const_hash(s: &str) -> u64 using the FNV-1a algorithmconst fn const_log2(n: usize) -> usize for bit-shift computationsconst fn const_min/max/clamp for bounded configuration valuesconst fn (bitops, arithmetic, loops, if)const fn can generate switch tables: const ESCAPE_CODES: [&str; 8]Code Example
pub const fn const_log2(n: usize) -> usize {
(usize::BITS - n.leading_zeros() - 1) as usize
}
pub const fn const_next_pow2(n: usize) -> usize {
1 << (usize::BITS - (n - 1).leading_zeros())
}
// Computed at compile time
pub const LOG2_256: usize = const_log2(256);Key Differences
const_log2(N) to compute shift amounts for power-of-two rings at compile time; OCaml computes this at runtime.const fn const_hash(s: &str) enables compile-time string→u64 mapping; OCaml has no equivalent.const fn cannot call println!, allocate, or use dyn; OCaml module-level code can call any function.OCaml Approach
OCaml lacks compile-time evaluation for complex patterns. The cppo preprocessor handles conditional compilation. For hash maps, phf (Rust) has no OCaml equivalent — OCaml uses runtime Hashtbl. Code generation via Dune rules or ocamlopt plugins can produce pre-computed tables, but this is more complex than Rust's const fn. Jane Street's ppx_hash generates efficient hash functions but evaluates them at runtime.
Full Source
#![allow(clippy::all)]
//! # Const Eval Patterns
//!
//! Patterns for compile-time computation.
/// Compile-time string hash (FNV-1a)
pub const fn const_hash(s: &str) -> u64 {
let bytes = s.as_bytes();
let mut hash: u64 = 0xcbf29ce484222325;
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(0x100000001b3);
i += 1;
}
hash
}
/// Compile-time max of two values
pub const fn const_max(a: usize, b: usize) -> usize {
if a > b {
a
} else {
b
}
}
/// Compile-time min
pub const fn const_min(a: usize, b: usize) -> usize {
if a < b {
a
} else {
b
}
}
/// Compile-time clamp
pub const fn const_clamp(val: i64, min: i64, max: i64) -> i64 {
if val < min {
min
} else if val > max {
max
} else {
val
}
}
/// Log2 at compile time
pub const fn const_log2(n: usize) -> usize {
if n == 0 {
0
} else {
(usize::BITS - n.leading_zeros() - 1) as usize
}
}
/// Next power of two at compile time
pub const fn const_next_pow2(n: usize) -> usize {
if n == 0 {
1
} else {
1 << (usize::BITS - (n - 1).leading_zeros())
}
}
/// Count digits at compile time
pub const fn const_digit_count(mut n: u64) -> usize {
if n == 0 {
return 1;
}
let mut count = 0;
while n > 0 {
count += 1;
n /= 10;
}
count
}
/// Reverse bits at compile time
pub const fn const_reverse_bits(mut n: u32) -> u32 {
let mut result = 0u32;
let mut i = 0;
while i < 32 {
result = (result << 1) | (n & 1);
n >>= 1;
i += 1;
}
result
}
/// Population count at compile time
pub const fn const_popcount(mut n: u64) -> u32 {
let mut count = 0;
while n != 0 {
count += 1;
n &= n - 1;
}
count
}
/// Ceiling division at compile time
pub const fn const_ceil_div(a: usize, b: usize) -> usize {
a.div_ceil(b)
}
/// Align up to multiple at compile time
pub const fn const_align_up(n: usize, align: usize) -> usize {
(n + align - 1) & !(align - 1)
}
// Example compile-time computed values
pub const HASH_HELLO: u64 = const_hash("hello");
pub const LOG2_256: usize = const_log2(256);
pub const NEXT_POW2_100: usize = const_next_pow2(100);
pub const DIGITS_IN_MILLION: usize = const_digit_count(1_000_000);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_const_hash() {
// Different strings produce different hashes
assert_ne!(const_hash("hello"), const_hash("world"));
// Same string produces same hash
assert_eq!(const_hash("test"), const_hash("test"));
}
#[test]
fn test_const_max_min() {
assert_eq!(const_max(3, 7), 7);
assert_eq!(const_min(3, 7), 3);
}
#[test]
fn test_const_clamp() {
assert_eq!(const_clamp(5, 0, 10), 5);
assert_eq!(const_clamp(-5, 0, 10), 0);
assert_eq!(const_clamp(15, 0, 10), 10);
}
#[test]
fn test_const_log2() {
assert_eq!(const_log2(1), 0);
assert_eq!(const_log2(8), 3);
assert_eq!(const_log2(256), 8);
assert_eq!(LOG2_256, 8);
}
#[test]
fn test_const_next_pow2() {
assert_eq!(const_next_pow2(1), 1);
assert_eq!(const_next_pow2(5), 8);
assert_eq!(const_next_pow2(100), 128);
assert_eq!(NEXT_POW2_100, 128);
}
#[test]
fn test_const_digit_count() {
assert_eq!(const_digit_count(0), 1);
assert_eq!(const_digit_count(123), 3);
assert_eq!(DIGITS_IN_MILLION, 7);
}
#[test]
fn test_const_popcount() {
assert_eq!(const_popcount(0), 0);
assert_eq!(const_popcount(0b1111), 4);
assert_eq!(const_popcount(0b10101010), 4);
}
#[test]
fn test_const_align_up() {
assert_eq!(const_align_up(10, 8), 16);
assert_eq!(const_align_up(16, 8), 16);
assert_eq!(const_align_up(0, 8), 0);
}
// Compile-time tests
const _: () = assert!(const_log2(256) == 8);
const _: () = assert!(const_next_pow2(100) == 128);
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_const_hash() {
// Different strings produce different hashes
assert_ne!(const_hash("hello"), const_hash("world"));
// Same string produces same hash
assert_eq!(const_hash("test"), const_hash("test"));
}
#[test]
fn test_const_max_min() {
assert_eq!(const_max(3, 7), 7);
assert_eq!(const_min(3, 7), 3);
}
#[test]
fn test_const_clamp() {
assert_eq!(const_clamp(5, 0, 10), 5);
assert_eq!(const_clamp(-5, 0, 10), 0);
assert_eq!(const_clamp(15, 0, 10), 10);
}
#[test]
fn test_const_log2() {
assert_eq!(const_log2(1), 0);
assert_eq!(const_log2(8), 3);
assert_eq!(const_log2(256), 8);
assert_eq!(LOG2_256, 8);
}
#[test]
fn test_const_next_pow2() {
assert_eq!(const_next_pow2(1), 1);
assert_eq!(const_next_pow2(5), 8);
assert_eq!(const_next_pow2(100), 128);
assert_eq!(NEXT_POW2_100, 128);
}
#[test]
fn test_const_digit_count() {
assert_eq!(const_digit_count(0), 1);
assert_eq!(const_digit_count(123), 3);
assert_eq!(DIGITS_IN_MILLION, 7);
}
#[test]
fn test_const_popcount() {
assert_eq!(const_popcount(0), 0);
assert_eq!(const_popcount(0b1111), 4);
assert_eq!(const_popcount(0b10101010), 4);
}
#[test]
fn test_const_align_up() {
assert_eq!(const_align_up(10, 8), 16);
assert_eq!(const_align_up(16, 8), 16);
assert_eq!(const_align_up(0, 8), 0);
}
// Compile-time tests
const _: () = assert!(const_log2(256) == 8);
const _: () = assert!(const_next_pow2(100) == 128);
}
Deep Comparison
OCaml vs Rust: Const Eval Patterns
Compile-Time Functions
Rust
pub const fn const_log2(n: usize) -> usize {
(usize::BITS - n.leading_zeros() - 1) as usize
}
pub const fn const_next_pow2(n: usize) -> usize {
1 << (usize::BITS - (n - 1).leading_zeros())
}
// Computed at compile time
pub const LOG2_256: usize = const_log2(256);
OCaml
(* Computed at runtime *)
let log2 n = int_of_float (log (float_of_int n) /. log 2.)
let next_pow2 n = 1 lsl (log2 (n - 1) + 1)
let log2_256 = log2 256
Compile-Time String Hash
Rust
pub const fn const_hash(s: &str) -> u64 {
let bytes = s.as_bytes();
let mut hash: u64 = 0xcbf29ce484222325;
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(0x100000001b3);
i += 1;
}
hash
}
pub const HASH_HELLO: u64 = const_hash("hello");
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Evaluation | Runtime | Compile-time |
| Loop in const | N/A | while loop |
| Bit operations | Available | Available |
| Binary embedding | N/A | Values embedded |
Exercises
const_hash to implement a compile-time dispatch table: const HANDLERS: [(u64, &str); 4] = [...] mapping hashed command names to handler names.const fn is_ascii_uppercase(c: u8) -> bool and const fn to_lowercase(c: u8) -> u8 and use them in a const fn to_lowercase_str that operates on a fixed-size array.const fn encode_color(r: u8, g: u8, b: u8) -> u32 that packs RGB into a 24-bit constant and a complementary const fn decode_color(c: u32) -> (u8, u8, u8).